diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 0000000..adb6fcf --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,1989 @@ +Quisk Version 4.2.28 December 2023 +=================================== +This version includes a patch from Jon, AB8WU, to build Quisk on FreeBSD. Thanks Jon! +I fixed a bug in setting the AF level in Hamlib. + +Quisk Version 4.2.27 December 2023 +=================================== +This release corrects the Hermes-Lite2 antenna tuner logic. Jim, N1ADJ, has an +antenna tuner and is helping debug. Thanks Jim! + +Quisk Version 4.2.26 December 2023 +=================================== +This release has support for the Hermes-Lite2 antenna tuner protocol. The tuner button is +on the last row of the screen at the left. Please test. + +Quisk Version 4.2.25 November 2023 +=================================== +When using the Hermes-Lite2 at a remote location over WiFi, the Tx buffer may underflow. +I added another line to the Config/Status screen to show the number of Tx buffer errors. + +Quisk Version 4.2.24 November 2023 +=================================== +You can now specify the WSJT-X option "--rig-name" on the Configure WSJT-X window. It used to be +fixed at "quisk". Please test. + +I worked around a bug in wxPython that caused some screens to be too small. I polished the code +for the RQST and ACK bits in the Hermes-Lite2 protocol. + +Quisk Version 4.2.23 September 2023 +==================================== +I fixed a bug in the HiQSDR when the clock is not a multiple of 48 kHz. I added more support for +the IO board. + +Quisk Version 4.2.22 August 2023 +================================= +This version adds more support for the Hermes Lite2 IO board. + +Quisk Version 4.2.21 July 2023 +=============================== +This version includes Windows support for Python 3.11. It also has the new IO board register numbers. + +Quisk Version 4.2.20 July 2023 +=============================== +I fixed a bug in the HiQSDR radio that caused the CW sidetone to play when not in CW mode. + +Quisk Version 4.2.19 May 2023 +============================== +I added the Flex ZZAR command to the Hamlib interface. This controls the AGC level from -20 to +120 dB. +The levels are mapped to the Quisk AGC level 0.0 to 1.0. The Flex AGC operates as a limiter but the +Quisk AGC is a compressor, so you will need to experiment to find the correct ZZAR command. Also the +Quisk AGC is never off. The off setting just changes the AGC level. So to turn AGC off send another ZZAR +command, perhaps "ZZAR+120;". + +When using the AC2YD remote feature, the serial port PTT now works correctly. + +Quisk Version 4.2.18 April 2023 +================================ +I added some small changes to the I2C messages sent to the Hermes Lite 2 IO board. + +Quisk Version 4.2.17 February 2023 +=================================== +I fixed the PTT keyboard shortcut on Linux. +The "documentation" link at the top on the Help screen now works on all platforms. + +The "Split" feature now defaults to 1000 Hz for CW, and 3000 Hz for other modes. You can now lock either +the Rx or Tx frequency. + +Quisk Version 4.2.16 January 2023 +================================== +The CAT serial port can now be a real hardware port or a virtual port. See the Config/radio/Remote screen. +Please test. + +Quisk Version 4.2.15 January 2023 +================================== +The Alsa sound driver and the Windows sound driver with the "Fast Sound" option are fast enough to produce a +useful sidetone for CW operation. I improved the PulseAudio sound driver to produce a fast sidetone too. + +Quisk Version 4.2.14 December 2022 +=================================== +The new file quisk_hardware_fifisdr.py supports the FiFi SDR hardware. Thanks to Joe, LA6GRA. +I added the rig 2 Hamlib command "U TUNER 1" to control the Spot button. +The Quisk remote feature now works with the "Small" screen format. Please test. + +Quisk Version 4.2.13 December 2022 +=================================== +Python 2 was obsolete as of January 1, 2020. And the new code by Ben, AC2YD, needs Python 3. It is troublesome to write +code that runs on both Python 2 and Python 3. So it is time to stop supporting Python 2 in Quisk. Please upgrade to +Python 3. If you have both versions on your computer use Python 3 for Quisk. + +Some have reported that the remote Quisk graph stops after a long Tx. No graph data is sent during Tx, so perhaps the +open UDP port has expired. I added keepalive graph data that is sent at one second intervals. Let's see if that fixes it. + +I added support for the HiQSDR radio to Remote Quisk. I corrected some drawing errors on the config screens. I removed +the "Help with Radios" config tab and moved the text to the documentation available from the Help button. + +Quisk Version 4.2.12 November 2022 +=================================== +I made the Hermes LNA slider control value persistent. This make more sense than having it be a configuration setting. +When using Quisk Remote, band changes from WSJT-X now correctly set the HL2 filters. I fixed a bug in the SoftRock +amplitude/phase adjustment screens. I started work on using the "small screen" version of Quisk work as a control head. +Please test, as more work may be needed. + +Quisk Version 4.2.11 November 2022 +=================================== +This is an update to the new Quisk Remote feature by Ben, AC2YD. This version reduces the audio sample rate from 48 ksps +to 8 ksps. This should help with slow networks, but is is hard to predict the effect. Please test. + +If you have dropouts at the new sample rate, try increasing the "Play latency msec" on the Config/Timing screen. Quisk +buffers sound, and the buffer size can be adjusted. + +I fixed the problem with the Split Rx/Tx feature. Please test. Ben made some additional changes for Windows networking. + +Quisk Version 4.2.10 October 2022 +================================== +I improved the Config/Config and Config/TxAudio screens and added help buttons for all items. The File Record +button no longer overwrites an existing file. It creates a new file each time it is pressed. Enter a base name +and the files will be base001.wav, base002.wav, etc. Decide on a directory for these files, perhaps the Music +directory on your computer or a special directory. Files will accumulate there, and must deleted. +The File Play names will follow the File Record names so that pressing Play after Record plays the current recording. +I had to change a lot of logic for this, and there may be bugs. So please test. + +I fixed some bugs in the new Quisk Remote feature by Ben, AC2YD. The Spot slider is now initialized. I fixed the +bugs in the Split feature. Please test, and if there are any remaining bugs, please report them. I will add +a feature to reduce the sample rate from 48 ksps to 8 or 12 ksps as the next step. I didn't add it now because +it may cause problems. Maybe it will increase latency, or the added processing will be too much for a Raspberry Pi +used as a remote. + +Quisk Version 4.2.9 October 2022 +================================= +This is an update to the new Quisk Remote feature by Ben, AC2YD. I fixed the problem with the favorites screen +and the station buttons below the X-axis. I fixed the "Failure in OnFreedvMenu" bug. + +Quisk Version 4.2.8 September 2022 +=================================== +This is an update to the new Quisk Remote feature by Ben, AC2YD. + +Midi now works with the control head. For HermesLite2, the RfLna is correctly initialized. +I changed the ports to 4585:TCP control; 4586: graph data; 4587:radio sound and mic. This should +work correctly with NAT. + +The main documentation page docs.html which is available from the Help button now describes the remote feature +in more detail. + +Quisk Version 4.2.7 September 2022 +=================================== +This is an update to the new Quisk Remote feature by Ben, AC2YD. + +These features are due to Jaroslav, OK2JRQ: The clip indicator is now sent from the remote to the control head. +The remote radio IP address can now be a host name or an IP address. TCP and UDP ports are now distinguished +in the documentation. The control head sends initial graph data to the remote in order to establish a path through NAT. +The remote radio can now be operated as a normal radio unless there is an active connection to the control head. + +The Favorites screen now works. I fixed all the bugs I know of except the segmentation fault on RPi4 raspian +when closing Quisk. But I need help testing everything. Please test and let me know of any new bugs or bugs +I missed. + +Quisk Version 4.2.6 September 2022 +=================================== +This is an update to the new Quisk Remote feature by Ben, AC2YD. The frequency is now correctly set when +changing bands. Initial menu items are now set. Frequencies stay synchronized when changing from USB to CWU. +I changed the graph data from 8 bit to 16 bit. + +I improved the config screen visibility when using "Dark" mode. + +Quisk Version 4.2.5 September 2022 +=================================== +This is an update to the new Quisk Remote feature by Ben, AC2YD. There is no longer a need to make the screen +sizes of the control head and remote radio the same. Any size should be OK. If you changed your screen sizes +to match, please go back to running with different sizes. Then verify that the graph on the control head is correct. + +I fixed all the bugs I know about, but this is still beta software. Please test. + +Quisk Version 4.2.4 August 2022 +================================ +This version adds the missing ac2yd directory. It is present in the Quisk source distribution but +missing in the "pip" installs. + +Quisk Version 4.2.3 August 2022 +================================ +This is an experimental release to test the new Quisk radio remote control software written by +Ben Cahill, AC2YD. This allows Quisk running on a local PC to control a remote radio. The remote radio +is connected to a PC also running Quisk. Ben has tested this on SoftRock CW operation. The current test +version supports SoftRock and HermesLite2. + +Quisk Version 4.2.2 July 2022 +============================== +I am still working on the Hambib Rig 2 interface. I changed the response to chk_vfo to agree with Hamlib 4.4. +I did some work on FM demodulation. Please test. + +Quisk Version 4.2.1 June 2022 +============================== +I improved the Hamlib Rig 2 interface. Please continue to report any problems. To test, start Quisk. Then +in a separate window start "telnet localhost 4532". Send commands to Quisk with telnet and view the response. + +I made some improvements to the station line shown below the X axis on the graph. Quisk now has some support +for the Hermes Lite IO Board. + +Quisk Version 4.2.0 May 2022 +============================== +I added a patch from Jaroslav, OK2JRQ, to improve compatibility with Hamlib. I removed the Python function +set_transmit_mode() because it seems to be unused. I fixed a lockup problem that happens when the Hermes Lite2 key +is pressed during SSB operation. + +Quisk Version 4.1.96 May 2022 +============================== +These changes were suggested by Neil, G4BRK. I fixed the bug that touching the CW key in SSB mode locked +up Quisk in transmit mode. All the Record/Play file names on the Config/Config screen were already saved when +starting Quisk. The state of the Play device is now saved too. I didn't save the Record check boxes +because restarting Quisk may overwrite an existing file. When changing between CW and SSB, the transmit and receive +frequencies are moved by the CW tone frequency. That way if you tune in a CW signal in SSB, and then change to CW +the CW tone is unchanged, and you are ready to transmit. + +I changed the return from Hamlib GetVfo() from "VFO" to "Main" to fix a compatibility problem. + +Quisk Version 4.1.95 May 2022 +============================== +This is a minor release to fix a Midi issue. There is now a separate item on the radio Keys screen to +control the Midi PTT toggle. I started to add support for my IO Board. But since the IO board is not released, +the code does nothing. + +Quisk Version 4.1.94 April 2022 +================================ +This is a minor release to fix some Midi issues. The Config/Keys "PTT key toggle" item now controls the +Midi PTT control as well as the hot key PTT. That is, it controls whether the Midi button must be held down +for PTT, or whether one press turns PTT on and the next press turns it off. + +Quisk Version 4.1.93 December 2021 +=================================== +I fixed a bug in the FreeDV button. It now works like other mode buttons. + +Quisk can now use the WDSP library to add the additional functions NR2 (noise reduction) and SNB (noise blanker). +The WDSP library is optional, and you don't have to use it. The library ships with Quisk on Windows. For Linux +it is used by many other SDR software, and you may already have it. If Quisk can find WDSP, the NR2 button will +be active. If the NR2 button is grayed out, you can install WDSP on Linux as follows: + + git clone https://github.com/g0orx/wdsp.git + cd wdsp + make + sudo make install + +This is an experimental feature, and I can use some feedback on its use. + +Quisk Version 4.1.92 October 2021 +================================== +I changed the power meter on the Hermes Lite 2 from an average value to PEP. The FreeDV sideband and mode +are now restored on startup. + +I fixed a bug that results in errors on tx_level when adding a new radio. I fixed a bug in the new +PTT key logic. The Midi control now updates the Rf gain display. + +Quisk Version 4.1.91 October 2021 +================================== +I changed the Midi control of the Rit frequency so the center includes the CW tuning offset. I tested this +with the DJControl Compact that Ben, AC2YD, lent me. + +The PTT accelerator key was failing for some users. It turns out that wx.GetKeyState() is not available +on all systems. So I had to program around the problem. If you have errors with GetKeyState(), +change "PTT key toggle" to True and "PTT key if hidden" to False. This is the only combination that +works with a bad GetKeyState(). While I was at it, I made changing the keys immediate (no need to restart). + +I made a change to fix the problem with "digital_rx2_name" not being found. I don't understand what +causes this, but the new code is more robust. + +I added a button to set the WSJT-X path and config option. It is on the Config/Config screen. + +Quisk Version 4.1.90 October 2021 +================================== +I changed the Midi feature to accommodate more Midi controllers. I removed the Midi message from +the Config/Status screen and moved it to the Midi screen (Config/radio/Keys). You can directly +assign the Midi message to a Quisk control, and the channel is now recognized. I added the RfLna control. + +I added a log file to Quisk. Log messages are sent to the file quisk_logfile.txt which is located with other +Quisk user files. The item "Debug level" on the Config/radio/Options screen controls the output. +A debug screen is shown when "Debug level" is greater than zero. + +The serial port PTT feature was not working. I fixed it and made some other improvements. Please test. + +Quisk Version 4.1.89 October 2021 +================================== +I fixed the problem of Quisk crashing when the Config button is pressed. This only happens on the +Raspberry Pi. I needed to work around a problem with wxPython on the Pi. + +Ben, AC2YD, lent me the Midi controller DJControl Compact, and I was then able to +add some improvements to the Midi feature. Midi now works for all buttons on the Small Screen layout. +I added control messages for the Volume, Ys and all other Quisk sliders. I added "Tune" to the controls. +Midi has two kinds of controllers. A "Knob" rotates a whole turn left and right and sends its level as +a number 0 to 127. A "Jog Wheel" rotates around and around, and sends up and down messages every few degrees. +I added both types of control. + +Quisk Version 4.1.88 October 2021 +================================== +This version makes digital programs like WSJT-X easier to use. There is a button on the Config/Config +screen to start WSJT-X. The default is "Never". Select "On startup" to start WSJT-X when Quisk starts. +Select "Now" to start it now. When Quisk starts WSJT-X, it uses "--rig-name quisk" so that the settings +you make are saved separately. + +On Linux, Quisk can set up virtual sound card names, and I changed these names on the drop down lists. +For Digital Tx0 Input, choose "Use name QuiskDigitalInput" and then find this name in the WSJT-X sound output menu. +For Digital Rx0 Output choose "Use name QuiskDigitalOutput.monitor" and then find this name in the WSJT-X sound input menu. +See the new Quisk documentation http://james.ahlstrom.name/quisk/docs.html#Digital for details. + +Quisk Version 4.1.87 September 2021 +==================================== +The documentation now has a better description of how to make a custom hardware file. + +I made some changes to the new Midi feature. The Config/Status screen now shows all three bytes of the Midi +message. I moved the MidiHandler logic to its own file midi_handler.py. If you copy this to your config file, +your MidiHandler will be used instead. This enables you to completely control Quisk with Midi. +For example, you can support Control Change messages. Take a look at the comments at the top of midi_handler.py. +If needed I can add logic to the Keys screen to extend Midi for those not comfortable with Python. + +Quisk Version 4.1.86 September 2021 +==================================== +This is a bug fix release to fix a problem with the new Midi logic and Keys screen. Please test. + +Quisk Version 4.1.85 September 2021 +==================================== +I made some visual improvements to the configuration screens. This should make them easier to use. + +You can now assign Midi notes to Quisk buttons. For example, you can assign note 57 to the PTT button, and +assign 58 to the Mute button. See the Config/radio/Keys screen. These assignments are global; that is, common +to all radios. I also moved the existing "Midi CW key" from the CW screen to the new Keys screen. This feature +is not complete for the "Small Screen" format because some buttons need two presses. Let me know if this is a problem. + +You can see the Midi note numbers on the Config/Status screen as they are received. This is useful for discovering +which note is sent by each Midi key. + +Quisk has a new hardware file quisk_hardware_hl2_oob.py. You can specify this name as your hardware file on the +Config/radio/Hardware screen. It is for use with the Hermes Lite 2, and it disables the power amp when the transmit +frequency including sidebands is out of band. Be sure to set accurate band edges on the Config/radio/Bands screen. +You might want to set a new band plan on the Config/Config screen. + +Quisk Version 4.1.84 August 2021 +================================= +Chuck Ritola submitted a patch to correct the phase adjustments in conjunction with channel delay. Thanks Chuck! + +I added the Transverter Offset from the Bands screen to SoapySDR. + +Quisk shows a color bar on the frequency X-axis to show the band plan. That is, the CW, phone and data segments +of the band. Since the band plan varies with country, there is a new feature to change it on the config screen. +Press the Config button and then the Config screen. Look for the "Band plan" button. It will bring up a screen +to set a list of frequencies and the mode (CW, phone) that starts at each frequency. The screen will start at +the band plan you are currently using. The band plan is the same for all your radios. You can mark any frequency +you want, even frequencies outside the ham bands. + +Quisk Version 4.1.83 June 2021 +=============================== +This version contains patches by Mooneer, K6AQ, for FreeDV mode 700E. Thanks Mooneer! + +Quisk generates its own CW waveform when keyed by the serial port or MIDI. Quisk delays this CW waveform +so that when changing from Rx to Tx there is time for relays to switch and power amps to turn on. +The CW key timing is preserved. This delay was 15 milliseconds, but is now adjustable. +It is controlled by the "Start CW delay msec" field on the Timing screen. Note that this does not work +if the key is connected to the hardware and the hardware generates the CW itself. + +When switching from Rx to Tx in all modes except CW, Quisk zeros the RF output for a few milliseconds +to allow time for relays to switch, power amps to turn on and filters to fill with samples. This time was 100 +milliseconds, but is now adjustable. It is controlled by the "Start SSB delay msec" field on the Timing screen. + +When Quisk is remote and controlled by another program it is possible that Quisk will be in the transmit state +when the link is broken. To prevent Quisk from transmitting forever there is now a timeout "Max Tx seconds" +on the Timing screen. This should normally be set to zero to disable the timeout. + +Quisk Version 4.1.82 May 2021 +============================== +Quisk now works with 64-bit Python 3.9. It is OK to continue using 64-bit Python 3.8, but please upgrade Python +from earlier versions. + +I changed PortAudio and added addition log messages. This is important for Apple Mac. + +Quisk Version 4.1.81 April 2021 +================================ +Quisk on Windows now handles devices with 3-byte samples, and works in both Exclusive and Shared mode. +I tried to catch up on all bug fixes. If I missed any please re-post. + +Quisk Version 4.1.80 February 2021 +=================================== +This is a bug fix release. I fixed a problem with sound on Windows when "Fast sound" is False. + +Quisk Version 4.1.79 February 2021 +=================================== +This is a bug fix release. I tried again to fix "Failure to convert device name" on Windows. I fixed the +5 second freeze in CW mode for SoftRock hardware. + +Quisk Version 4.1.78 February 2021 +=================================== +Hamlib Rig2 split VFO now controls the frequency of the first added receiver. This is used for working satellites. +Use the regular Quisk window for the uplink, and the extra receiver for the downlink. Control both frequencies +with Hamlib. The Hamlib "F" command controls Rx, and the "I" command controls Tx. + +I added code by Ben Cahill, AC2YD, to recover from a fault in the playback thread for "fast sound" on Windows. +I fixed a bug in the HermesLite 2 Small Screen layout. I added address 0x09 bit 17 for the HermesLite 2 radio. + +I may have fixed the problem "Failure to convert device name" on Windows. If it is not fixed, the new error +message should be more informative. + +Quisk Version 4.1.77 January 2021 +================================== +I added further changes to support the YU1LM designs and the Genesis 3020 QRP rigs. When using the ZZBS Hamlib +command, Quisk now remembers the last frequency and mode. I added back some softrock files that were removed. + +Quisk Version 4.1.76 January 2021 +================================== +If there are errors when Quisk starts on Windows, the window showing the error messages now stays open +so you can read them. It used to close too fast. + +I improved sound buffer levels so they stay closer to 50%. Ben Cahill, AC2YD, contributed code and testing +support. I corrected Quisk C code so no warning messages are produced by the latest compiler. + +I fixed the problem with Hermes write queue timeouts on startup. But these will still appear if the Hermes +is not running. + +I added back the RTS signal to the Linux serial port CW keying logic. This signal goes high when DSR goes high. +When DSR goes low, RTS goes low after a 1.5 second delay. + +Dr. Karsten Schmidt contributed additional logic to the Hamlib serial port. Thanks Karsten! +I added two new Hamlib commands: ZZAG and ZZBS. + +Quisk Version 4.1.75 December 2020 +=================================== +This version includes improvements to the Windows sound code contributed by Ben Cahill, AC2YD, and a fix +to the Macintosh contributed by Christoph, DL1YCF. Thanks! + +The pip install method on Linux now builds the Afedri and Soapy modules properly. Please test. + +Quisk Version 4.1.74 December 2020 +=================================== +I added some missing controls to the Hermes-Lite2 Hardware configuration screen. I removed the Alex filter +band screen and implemented the same logic in the regular Bands screen. If you need the Alex screen, +please complain. + +Quisk now ships with Linux binary files for Python3, not Python2. If you have Python2, run "make" +to make a Python2 version. You could also start using Python3 instead. + +Eric, WW4ET, Davide Gerhard, and Christoph, DL1YCF, provided help to make Quisk run on a Mac. Thanks! +Christoph provided code. Thanks Christoph! The Mac changes may not be complete. Please test. + +SdrIQ support was changed. Quisk now uses the hardware file quisk_hardware_sdriq.py instead of sdriqpkg. +The directory sdriqpkg and those binary files are now obsolete. The new code is 100% Python. + +Quisk Version 4.1.73 November 2020 +=================================== +I rewrote the logic for SoftRock I/Q amplitude and phase corrections. The new logic will enable much better +image suppression because it can correct I/Q based on both the VFO and the tuning offset. If you do nothing, +there is no change in the corrections. To use the new logic, enter new correction data. Use the Help button +on the Config/radio/Config correction screens. Corrections are in the file quisk_init.json, and you should +make a copy of this file to save your current corrections. + +I added a missing version.h file to SoapySDR. It is not clear that this fixes the version check for the +change in the Soapy API. Please test. + +When using the Split button, Quisk now leaves the Rx frequency unchanged and splits the Tx frequency. + +I added a new band "Aux1" that can be checked on the Config/radio/Bands screen. It can be used as a special +band for a panadapter or when Quisk is used as a 10.7 MHz IF. + +Quisk Version 4.1.72 September 2020 +==================================== +For SoftRock hardware, the Tx power can now be set for each band. See the Config/radio/Bands screen. I also +added the Tx level and Digital Tx Level sliders to the Config/Config screen, and the Digital Tx power % +to the Config/radio/Hardware screen. + +The Y-scale and Y-zero settings are now saved for the graph at the top of the Waterfall screen. These settings +are independent of the settings for the Graph display. + +I fixed a problem with the Hermes Lite 2 SWR display. I fixed a problem with SoftRock phase corrections. + +Quisk Version 4.1.71 September 2020 +==================================== +The last band and frequency etc. are saved in the file .quisk_init.pkl, but this binary file can not be +easily read. I am now writing initialization in the new file quisk_init.json, a text file. The location +of this and the other Quisk user files is now shown on the Config/Status screen. + +I fixed a bug in the new CW snap-to-peak feature. I added "PTT hang time" and "Tx buffer msec" to the +Hermes Lite 2 hardware screen. + +The Graph button can be pressed repeatedly to select display averaging. The Waterfall button can now be +pressed repeatedly to select averaging for the graph at the top. Remember that the Ys and Yz sliders on the +Waterfall screen adjust the waterfall colors. To adjust the graph at the top, hold down the Shift key. + +Quisk Version 4.1.70 August 2020 +================================= +I speeded up MIDI CW keying on Linux. + +There used to be two ways to set the level of Tx audio sent to SoftRock hardware, a slider or the +"Tx audio level" on the Config/radio/Hardware screen. I removed the slider. Please set the level on +the Hardware screen. I removed the 70% limitation and you can now set any level you want. + +For CW modes, if you click a peak within the filter bandwidth, the tuning frequency will snap to +the peak. This is an aid to tuning in CW signals. You can still adjust the frequency with the mouse +wheel or by dragging. I also made frequency rounding work with the Rx frequency in split mode. + +Quisk Version 4.1.69 August 2020 +================================= +Quisk can now use a MIDI device for CW keying. The setup options are on the Config/radio/Timing and CW +screen. Just enter the MIDI name and note number and restart. Performance is good on Windows but poor +on Linux. See my paper http://james.ahlstrom.name/serialports/index.html for measurements. +If you use Linux, try the aseqdump command for testing. + +I fixed a bug in the small screen display. + +When using "Debug sound" on Windows, I was surprised to find that clicking the console (terminal) +window froze the sound. I had to move all debug messages to an extra window and avoid writing to +the console. This is the oddest Windows "feature" I have seen. + +I did some more work on Windows "fast sound" stability, and it runs for hours at my QTH. There +may still be problems on other hardware, so please test. + +Quisk Version 4.1.68 August 2020 +================================= +I fixed a bug in the S-meter code for very small signals of less than 1E-16. I fixed a bug +in the config screen colors when Quisk can not find the correct color scheme of the system. + +I changed the Windows "fast sound" system again. This should offer better performance +and lower CPU usage. Please test by setting "Debug sound" to 1 on the Config/radio/Options +screen. + +Quisk Version 4.1.67 July 2020 +=============================== +I improved the Windows CW operation for SoftRock. Windows sound should now be stable when +using "fast sound". Please test. + +I fixed the colors on the config screens when using the "Adwaita dark" theme. The hardware +CW/PTT input now works on voice modes for HiQSDR. + +Quisk Version 4.1.66 July 2020 +=============================== +This version changes the Linux sound system. Soundio is no longer used. Instead I rewrote +the old logic and added fast sound to it. The fast sound is only used for the sidetone. +There is no longer a "fast sound" option on the config screens for Linux. Please test. +The Windows sound system was not changed. I will work on that next. + +Thanks to Dave Roberts I corrected the Soapy module for a recent API change. Thanks Dave! +Thanks to Jaroslav, OK2JRQ, I fixed two bugs in the Python2 version. Thanks Jaroslav! +I added back the waterfall_scroll_mode item from the config file. + +Quisk Version 4.1.65 July 2020 +=============================== +This version should correct the problems with opening soundcards. To open a Delta 44 use the +name "alsa:plughw:CARD=M44,Dev=0". You can use any Alsa name that libsoundio recognizes. There +are other improvements to the sound system to increase stability and reduce CPU usage. + +Quisk Version 4.1.64 July 2020 +=============================== +The favorites button now jumps to the correct screen. The Pulse device "default" now works. +This version should open "fast sound" devices that failed before. Look on the Config/Status +screen to see if all devices open successfully. If there are still problems, the new debug +messages should add more information. Turn on "Debug sound". Then start Quisk with and +without "fast sound". Save the startup messages. + +Quisk Version 4.1.63 June 2020 +=============================== +This version fixes an incompatibility with Python2 from version 4.1.61. I plan to support Python2 +indefinitely, but be advised that Python2 is obsolete. It would be best to upgrade to a 64-bit +version of Python3. + +Dave Roberts, G8KBB, provided code to add new FreeDV modes. But the newest 2020 mode requires an +additional shared library that Quisk does not provide. Quisk provides a codec2 but multiple +dependent libraries should be installed in the correct system directories. So to use 2020 +you need to install a new codec2 and all its dependencies. Then you can use 2020 with Quisk. + +Quisk Version 4.1.62 June 2020 +=============================== +This version fixes problems with fast sound on Windows. If you are not running Windows there is no +need to update. + +Quisk Version 4.1.61 June 2020 +=============================== +This version fixes a problem with the latest version of wxPython on Windows. Quisk works with all prior +versions of wxPython, but there was a problem with the newest 4.1.0. Buttons did not work properly and +stayed pressed. I think this is a problem with wxPython, but I found a workaround by re-writing all the +waterfall code in C. I do not know if this problem occurs on Linux because my Ubuntu 18 has wxPython +version 4.0.1. But now Quisk should work with any recent wxPython. + +Quisk Version 4.1.60 June 2020 +=============================== +This is a bug fix release. I fixed a problem with the file record and play buttons. +I fixed a problem with SoftRock CW operation. I make some major changes to softrock/hardware_usb.py. +If you use this file, SoftRock CW should work. If you are using a different hardware file, you should +check the changes and see if you need to modify your file. + +Quisk Version 4.1.59 June 2020 +=============================== +This is a bug fix release to correct some installation problems. + +The most recent wxPython version 4.1.0 is not compatible with Quisk on Windows. The buttons do not seem +to show the correct up/down state even though they work. For Windows, please install the last version: + +pip install wxPython==4.0.7.post2 + +For Linux, if you plan to use the new fast sound for sidetone, install libsoundio-dev before installing Quisk: + +sudo apt-get install libsoundio-dev + +Quisk Version 4.1.58 June 2020 +=============================== +This version includes a new faster sound system that enables fast CW sidetone and remote CW operation for +the Hermes-Lite2. All the old sound systems are still included, and you can turn the new sound on and off +from the Config/Radio/Timing_and_CW screen. New sound is default off. To use new sound on Linux, +install the libsoundio-dev package with "sudo apt-get install libsoundio-dev" and run "make" to rebuild Quisk. +New sound runs on Windows, but interferes with the screen update. Sometimes buttons appear to stay pressed although +they work fine. Maybe someone can offer a solution. + +When using the new sound, adjust your play latency and data poll parameters for best operation. Start with 150 +milliseconds play latency and 10000 microseconds data poll. + +I fixed a bug with the external demodulation button. I moved the sound latency timing parameter +from the sound screen to the timing screen. I removed the top level "Config/Sound" screen because it +was not useful and conflicted with the radio sound screen. The "quisk" command in Windows should now +start Quisk. + +Andrea, IW0HDV, provided a patch for the Perseus hardware. Thanks Andrea! +Eoin Mcloughlin, EI7HSB, provided a patch to speed startup time. Thanks Eoin! +Dave Roberts, G8KBB, provided code to add new FreeDV modes. Thanks Dave! +For SoftRock, Quisk will now try the USB backend libusb0 if libusb1 fails. Thanks to Ben Cahill! + +I added the "-r" or "--radio" command line option to choose the startup radio. To get a list of +command line options, use "python quisk.py --help". + +Quisk will now save and restore the various record and playback file names. You can now right-click +the temporary playback button to save the sound to a file. + +For Hamlib I added additional control items to Quisk's rig2 protocol. The mode PKTUSB sent by WSJT-X is now +recognized and interpreted as mode DGT-U. I also added PKTLSB and PKTFM. I added the level AGC to +control the AGC button, and the level AF for the audio volume. This work makes it easier to add additional +Hamlib controls in the future. + +There are a lot of changes in this release. Please report bugs to the N2ADR-SDR group at https://groups.io/g/n2adr-sdr. +A detailed description of the bug would be good. A patch would be even better. If you have problems with this release +remember that you can install the prior version: + +To install the prior version on Windows: pip install quisk==4.1.57 +To install the prior version on Linux: sudo -H pip install quisk==4.1.57 + +Quisk Version 4.1.57 May 2020 +============================== +This is a bug fix release. I fixed a bug in the small screen version. I fixed an incompatibility +with the latest wxPython version 4.1.0. I added a patch from Dave Roberts to close FreeDV properly. +Thanks Dave! + +Quisk Version 4.1.56 April 2020 +================================ +For Linux, if the file /usr/include/portaudio.h is absent, portaudio will not be included in Quisk. +This change makes portaudio optional. The change was needed because portaudio breaks wine. Quisk now +restores the correct frequency for the 60 meter band. There were some other bug fixes. + +Quisk Version 4.1.55 April 2020 +================================ +The new Sdr Micron radio was missing from the Windows version of Quisk. There are reports that the item +Hermes_BandDictTx is damaged in the quisk_settings.json file. Quisk will now fix this itself. The item is +the "Tx IO Bus" on the Hermes Bands configuration screen. + +Quisk Version 4.1.54 March 2020 +================================ +There are now two new radios available in Quisk. David Fainitski contributed code for the Sdr Micron, +and Andrea Montefusco IW0HDV contributed code for the Perseus SDR. The radios should appear on the +list of supported radios in Config/Radios screen. Check the Config/radio/Hardware screen to see if you +need any more options. Please test these radios to make sure everything is working. + +I added a general way to add new radios to the configuration screens. See docs.html under Custom Hardware. + +Quisk Version 4.1.53 March 2020 +================================ +I changed the Afedri radio module to be compatible with Python3. The graph Zoom feature now is available +on the Bandscope screen. Vladimyr Burdeiny provided a patch to make dx cluster work with Python 3. + +I extended the range of the Waterfall color controls Ys and Yz to accommodate hardware with a higher +noise floor such as PlutoSDR. You will have to readjust your waterfall colors. + +I added a hardware setting for the LNA gain during transmit for the HL2. I made some changes to the CW +and PTT hardware interface for the HL2. These work great with the HL2, but test the new code if you have +different Hermes hardware such as Red Pitaya. + +Quisk Version 4.1.52 December 2019 +=================================== +I added an On/Off button to Quisk. When Quisk starts the button is "On". When you turn it to "Off", +Quisk shuts down the sound system and closes the hardware. When you turn it "On" again, Quisk starts +with any new setting made from the Config screens. This provides a way to start Quisk with changed settings, +and to re-start your hardware after a power down. + +Martin Schaller found and fixed a bug in the Alsa sound system that produced sporadic crashes. Thanks Martin! +Max, G7UOZ, found and fixed a bug in the Python3 SoftRock hardware_net.py code. Thanks Max! +This Quisk includes a more recent FreeDV library in both 32-bit and 64-bit versions. The Windows +SoapySDR interface still requires 64-bit Quisk. + +The Config/Status screen now shows the dB level of each open sound device. The maximum level is zero dB. +This is meant as an aid to configuring the sound devices. + +Quisk Version 4.1.51 November 2019 +=================================== +I fixed a bug in the "small screen" code when using the new wx version 4. Thanks to Martin Schaller for +pointing out that underflows from the Soapy sample source should not count as errors. + +This version fixes the Linux installation errors "ImportError: No module named _quisk". It also provides +a way to run either Python2 or Python3 quisk when both are installed. To do that use "python2 -m quisk" +or "python3 -m quisk". + +The new installation instructions are here: http://james.ahlstrom.name/quisk/docs.html#Installation + +Quisk Version 4.1.50 November 2019 +=================================== +Thanks to Martin Schaller for fixing a bug in the SoftRock Tx sound level. I fixed a bug in the HermesLite +gateware program button traceback. + +This Quisk provides pre-built binaries for the Windows Python3 versions. There are now four Windows versions: +Python2 and Python3, each for either 32 or 64-bit Python. If you have a recent Python3, just run your Python3 +and use pip to install Quisk. Pip will detect the version of Python you are using, and will install the +correct version of Quisk. If you want to try Python3, install the latest 64-bit version. +The installation instructions are here: http://james.ahlstrom.name/quisk/docs.html#Installation + +Quisk Version 4.1.49 November 2019 +=================================== +Quisk can now program the Hermes Gateware (FPGA flash) over Ethernet. See the button on the radio Hardware screen. +I fixed some more Python 3 bugs. + +Quisk Version 4.1.48 November 2019 +=================================== +I fixed some more problems with the Python3 version of Quisk. I added support for reading and writing the +EEPROM in the HermesLite. Reading the EEPROM requires code version 68 or newer. The EEPROM settings are on +the radio Hardware screen. + +Quisk Version 4.1.47 October 2019 +================================== +This is a bug fix release. I fixed a problem with the Python 3 version of Quisk saving program state. I +fixed a sound device name problem on Windows. I added some code provided by Steve to support the HermesLite. + +Quisk Version 4.1.46 October 2019 +================================== +When using the bandscope screen the band buttons no longer reset the frequency and mode. Try it. +Thanks to Ed, GM3SBC, for changes to the hot key PTT logic. The hot key now works better, and is disabled +when using the config screens. I changed the FM repeater offset code to increase the level of the CTCSS tone. + +I made further progress on Python3 and fixed some resultant problems on Python2. The objective +is to have a Quisk that runs the same on Python3 as Python2 so you can use either Python. Linux users +can test the Python 3 version by entering "make quisk3" followed by "python3 quisk.py". + +Quisk Version 4.1.45 September 2019 +==================================== +I added a config screen hardware option "Hermes known IP". If you know the IP address of the Hermes +hardware, enter it here. Otherwise Quisk will search for the hardware using the usual broadcast method. + +The Python Rx samples interface can now accept bandscope data, that is, raw samples from the ADC. See +the file quisk_hardware_model.py for documentation. I fixed a bug in the bandscope that added noise. + +Quisk Version 4.1.44 September 2019 +==================================== +I made some improvements to the Python Rx sample interface. See quisk_hardware_model.py for the +new interface. I removed the duplicate decimation rates from the SDR-IQ hardware screen. +There are a number of changes to make Quisk run on Python 3, although this is not finished yet. + +Quisk Version 4.1.43 August 2019 +================================= +This release fixes a bug that resulted in calling the hardware HeartBeat() method too frequently. +This caused flickering of the HermesLite temperature reading. + +Quisk Version 4.1.42 August 2019 +================================= +I improved the SoftRock amplitude/phase correct screen. I fixed a bug in the Hamlib extended commands +thanks to James, KE4MIQ. + +I added an interface to provide Rx samples to Quisk directly from the Python hardware file. Many new SDR +hardware devices return samples by Ethernet, USB or a serial port. Since Python has these three interfaces, +it may be possible to add a new hardware device to Quisk by using Python alone. This is much easier than +adding C code to Quisk. This work was inspired by David, N7DDC, an avid SDR hardware designer. + +I am using the new interface to support the SDR-IQ by RfSpace. Change your hardware file to +quisk_hardware_sdriq.py instead of the sdriqpkg directory to use the new logic. The sdriqpkg +directory will be removed in a future release. + +Quisk Version 4.1.41 June 2019 +=============================== +I added Steve's code for the Hermes Lite 2 that moves the harmonics of the switching power supplies +out of the amateur bands. Thanks Steve! I renamed sound devices to have consistent names on all screens. +Stephen Hurd contributed changes to the SDR-IQ module and the Quisk widgets module. Thanks Stephen! +I made the configuration screens for the radios more efficient. I may have fixed the last of the bugs. +Please test. + +Quisk Version 4.1.40 June 2019 +=============================== +This is a bug fix release for assorted problems. The filter bandwidth is now saved for program start. +I fixed a buffer overflow in the DirectX code. I added "Digital output level" to the Config/Sound screen. + +I was not able to duplicate a burst of Tx noise when switching to digital modes. But I added code +to fix it anyway. Please test. + +I was not able to duplicate the problem that the Spot output frequency is correct, but the audio is +always transmitted at the center frequency instead of the tuning frequency. Please re-test with this +release, and if it is not fixed I will try again. + +Quisk Version 4.1.39 May 2019 +============================== +I fixed a bug in the VNA program quisk_vna.py. The Windows version of SoapySDR is compiled +against SoapySDR version 0.8.0. + +Quisk Version 4.1.38 April 2019 +================================ +The bandwidth of the DC removal filter on the Config/radio/Options screen now applies to all sample +sources, not just sound cards. This is useful to set the bandwidth for SoapySDR devices. For +Hermes Lite, set the bandwidth to zero or one. + +I added Rx and Tx sample rate and bandwidth to the SoapySDR hardware screen. Any value can be entered, +not just the values from a list. See the latest SoapySDR news at http://james.ahlstrom.name/quisk/soapy.html. + +Quisk Version 4.1.37 April 2019 +================================ +I added another broadcast address to the Hermes code to fix a network problem. I added an option to +the radio Options config screen to reverse the sideband on Tx. This is needed for satellite operation. +I added the 13, 9, 5 and 3cm bands to the list. I added the additional samples rates 50, 100, 250, +500 and 1250 ksps to accommodate the RedPitaya. The frequency entry box now clears its entry with +each use. You can now specify the bandwidth of the SoftRock DC removal filter on the radio Options screen. + +I added some of the SoapySDR transmit parameters on the radio screen. But I am having trouble making +transmit actually work. So transmit is disabled for now. + +Quisk Version 4.1.36 March 2019 +================================ +I added a patch by Franco Spinelli, IW2DHW, to add chk_vfo to the Hamlib control for WSJT-X. +There are further improvements to the SoapySDR radios. I added three Rx gain setting methods +and automatic DC correction. You will need to re-read the SoapySDR device. Just use the +"Change.." button and leave the device name unchanged. This is necessary to read additional +device parameters from the hardware. + +Quisk Version 4.1.35 February 2019 +=================================== +I added the Tx Level to the bands config screen for the Red Pitaya. Now you can set the power level +for each band. I changed the Hermes Lite networking so it works with Virtual Box and multiple +network interfaces. Please test. + +I am starting to add SoapySDR support to Quisk. See http://james.ahlstrom.name/quisk/soapy.html. + +Quisk Version 4.1.34 January 2019 +================================== +This fixes a bug in version 4.1.33 that caused a traceback for b_test1 when using the Small Screen +layout. I also added support for Afedri hardware. Just use the Config/Radios screen and add +a radio of type Afedri. + +Quisk Version 4.1.33 January 2019 +================================== +I improved the output level control for digital modes. This removes the lag in attaining 100% +level. For the first transmit, Quisk does not know what level to expect from the external digital +source, so there may still be a small delay. + +I added a squelch feature that works for SSB and FM. Just push the "Squelch" button. The SSB level +automatically adjusts for band noise, and the default slider value of 0.200 should be close to +what you want. + +Quisk Version 4.1.32 January 2019 +================================== +I corrected some button colors and colors on the config screens. The green Rx line is now centered +in the filter for Split mode. + +Quisk used to leave some headroom in the 16-bit samples it sends to the hardware. The maximum level +used to be about 70%. I changed the maximum level to 100%. So the PEP output for all modes will be +the same level as 100% Spot output. But remember that the PEP level is 100%, not the average level. +An average reading power meter will show a level much below PEP for SSB and digital modes. + +There is a new tuning method available. If the Shift key is held down when you left-click the graph, +the Rx filter is centered at that frequency. Normally the Tx frequency is set at the frequency. +This is useful when using narrow digital filters. It can also be used for SSB. + +Quisk Version 4.1.31 December 2018 +=================================== +When using N1MM+, I removed the 10 kHz frequency shift when adjusting frequency. The center will now +shift only if the external frequency control gets within 10% of the left or right edge. Then the +frequency is re-centered. + +I fixed the new Windows version handling of Latin-1 accented characters. I added a new "Filters" page +to the Config Radio screens for the Hermes Protocol Alex high pass and low pass filters. + +Quisk Version 4.1.30 December 2018 +=================================== +The Windows installation procedure is different. If you have an old Quisk installed, you should use +the Windows Apps screen to uninstall it. Then install Quisk using Python "pip". See the downloads page +and the new documentation. The new installation method is compatible with standard Python setup tools. +The Linux installation procedure is also different. + +I removed the "pyusb" module from Quisk. You will need to install the public version if you need it. +The inclusion interfered with the public version. See the downloads page and the new documentation. +I fixed a bug that happens with multiple displays. The zoom feature now centers in the Rx passband. +This is useful for narrow digital modes. + +There are a lot of changes in this version, and the installation method is new, as is the 64-bit +Windows version. If you have any problems or suggestions, please post to the usual groups. + +Quisk Version 4.1.26 November 2018 +=================================== +I added some narrow filters to the digital modes DGT-U and DGT-L. The narrow filters are +centered at 1500 Hertz. This makes filtering in the digital program easier, and is +useful for RTTY. As part of this project I rewrote the filter display logic. Filters are now +more accurately shown, and include the effect of RIT. + +The keyup delay on the radio Timing screen used to affect only the CW key. It now works with +the PTT button too. + +Quisk Version 4.1.25 October 2018 +================================== +I added the option of Quisk CAT control by a software serial port to accommodate N1MM+ logging software. +Set the N1MM radio to "Flex", and enter the port name on the Quisk Config/radio/Remote screen. +You need a "Virtual Serial Port" (VSP) pair. One side connects to Quisk and the other to N1MM. +For Linux, Quisk can set up these ports itself. Pick a port name like "/tmp/QuiskTTY0" on the +Config/radio/Remote screen and enter that name in the external program. On Windows +you need a VSP that is set up by an external program. This is like the "Virtual Audio Cable" +needed for samples. An Internet search will turn up HDD Software, Eltima Software and many others. +Set up a port pair, and enter one name on the Quisk Remote screen, and the other name in N1MM. +Not all of the "Flex" commands are implemented. If you get any error messages, send them to me. + +I moved the split Rx/Tx play menu from the config screen to the Split button so it is easier to change. +I added code from Steve Haynal to control the VersaClock on the Hermes-Lite2. + +Quisk Version 4.1.24 October 2018 +================================== +I changed the sound logic so that a mic device is no longer needed for digital modes or Spot. The mic +is only needed for voice communications. I added a requested feature to the Config/radio/Keys screen. +I improved the operation on the Raspberry Pi. + +Quisk Version 4.1.23 September 2018 +==================================== +I fixed some more problems with the new Ubuntu 18.04 LTS. + +Quisk Version 4.1.22 August 2018 +================================= +I fixed the FM squelch so that it works for multiple receivers. + +The record to file and play from file logic was simplified and improved. See the Config/Config +screen and the Help button. Quisk can now both record and play an IQ samples file. I removed +the 4 Gig file size limitation inherent in a WAV file so that larger files can be used. + +I fixed some of the problems with the new Linux 18.04 LTS. There is still an assertion error when accessing +the config screens. I am trying to work around it, but it seems to be a bug in the gnome desktop. + +Quisk Version 4.1.21 July 2018 +=============================== +I made additional changes for the Hermes Lite 2 project. Control the CW hang time with the +Config/Radio/Timing/Tx_delay_msecs option. I added a change for the Dx Cluster feature +from YO5RXM. Thanks! + +Quisk Version 4.1.20 June 2018 +=============================== +I made additional changes for the Hermes Lite 2 project. I added a change for the Dx Cluster feature +from YO5RXM. Thanks! I fixed a graph screen flicker problem on Windows. + +Quisk Version 4.1.19 June 2018 +=============================== +I made additional changes for the Hermes Lite 2 project. There are now separate Rx and Tx filter +settings. If multiple receivers are in use, the Rx filter is that of the highest frequency band. + +Quisk Version 4.1.18 June 2018 +=============================== +I added improved code for FreeDV to support the new 700D mode. + +Quisk Version 4.1.17 May 2018 +============================== +The Quisk VNA program now works with Hermes-Lite 2. Firmware 62 and above is required. +I made additional changes for the Hermes Lite 2 project. + +Quisk Version 4.1.16 May 2018 +============================== +I updated the built in version of FreeDV digital voice. It now includes newer modes such as 700D. +I added the column for the IO bus to the Red Pitaya radio. I fixed a bug when selecting the +"Bands" screen. I added shortcut keys to the buttons. If a button has an underlined letter, +pressing ALT and the letter will press the button. This is convenient for operation, and allows +Quisk to be controlled by another program that can generate key presses. If you have set a PTT +shortcut key, please check its validity on the radio Keys screen. The shortcut characters for the +band buttons can be changed in the config file. + +Quisk Version 4.1.15 March 2018 +================================ +I changed the logic for the PTT keyboard shortcut. If you are using a PTT shortcut key, +go to the Config/Radio/Keys screen and check the keys you are using. I made further changes +to support Hermes-Lite. And the "darwin" platform was added to defaults. I fixed the +Status screen flickering that occurred on Windows. + +Quisk Version 4.1.14 March 2018 +================================ +The use of the Spot button for testing has been removed, and proper operation restored. +There are further improvements to the Hermes-Lite power and current calculations, including a +correction to the PA current measurement from Steve. Thanks Steve! + +There is a new "Keys" tab in the configuration screens where you can set one or two hot keys +to press the PTT button. Space bar seems a popular choice. I added a toggle option so that +PTT stays on until the next key press. + +I made further improvements to wxPython 4.x Phoenix support, but feel free to +continue to use the 3.x version. + +Quisk Version 4.1.13 February 2018 +=================================== +This version is only for TEST. The Spot button now generates an audio tone from 0 to 5000 +Hertz controlled by the sidetone slider. +I added power calculations for the Hermes-Lite project. +Quisk now works with the wxPython 4.0 Phoenix release. This code is beta, so feel free to +continue to use the 3.x version. + +Quisk Version 4.1.12 December 2017 +=================================== +Sid, G3VBV, updated the documentation file. Thanks Sid! I fixed a network problem with the HiQSDR +on Windows 10. + +There is now an easy way to connect Quisk to an external digital program such as Fldigi or wsjtx by +using Linux PulseAudio. Just set the names pulse:QuiskDigitalInput and pulse:QuiskDigitalOutput +on your radio Sound configuration screen. Quisk will create the necessary loopback devices. Then +set your digital program output/input to Quisk input/output. Press the Help button for +documentation, and see the docs.html file for details. + +Quisk Version 4.1.11 November 2017 +=================================== +I added a fix for the Proficio USB frequency control from Stew, N8VET and Tony, VK4KRC. Thanks! +I changed the Config/Radio/Bands/IO Bus screen to a bit field instead of an integer for the Hermes Lite. +There is a new "Frequency round for SSB" on the radio/options screen. It rounds the left mouse click +to 1000 Hertz for voice modes on HF. This is useful when most voice stations are at a multiple of 1000 Hertz. +It defaults to zero, so enter 1000 (or other rounding frequency) to turn it on. +The graph Ys and Yz settings are now saved for each band. + +Quisk Version 4.1.10 August 2017 +================================= +I added Steve's Quisk updates for version 60 of the HL2 firmware. Thanks Steve! I fixed a bug in +the VNA code for HermesLite. But please note that quisk_vna will NOT work in HermesLite2 until I +have a working HL2 and I port the VNA firmware to it. Currently VNA requires a Hermes Lite with +firwmare version 32. + +Quisk Version 4.1.9 August 2017 +================================ +The "Display fraction" is ignored for the Bandscope so the whole spectrum is visible. I added +bandscope support for the Odyssey-2 radio. I changed the HermesLite2 logic to agree with the +new firmware. Thanks Steve! I added logic for the SoftRock to correct for rate differences +between two sound cards. Thanks to Nick, G3VNC! + +Quisk Version 4.1.8 June 2017 +============================== +This version adds a bandscope screen for the Hermes Lite. It uses the raw data +from the ADC to display a 36 MHz wide view of the spectrum. You do not need to +update to this version unless you have Hermes hardware. + +Quisk Version 4.1.7 June 2017 +============================== +This version merges all Hermes Lite 1 and 2 features with the regular Quisk distribution. +There is no longer a need for a special Hermes Lite version. If you have a Hermes Lite 2, +please use the latest firmware with version 40 and up. The extra data windows and settings for +HL2 will appear when using HL1, but will be invalid. I will fix this when everyone updates to +the latest HL2 firmware. + +Robert, DM4RW contributed a bug fix for CW. Thanks Robert! + +Quisk Version 4.1.6 May 2017 +============================= +This version fixes a bug in the small window format of the Hermes Lite 2 screen. +Do not use this version unless you have HL2 hardware. + +Quisk Version 4.1.5 April 2017 +=============================== +This version adds code for Hermes Lite Version 2 to display temperature and current. +Do not use this version unless you have HL2 hardware. + +Quisk Version 4.1.4 March 2017 +=============================== +This version adds code for Hermes Lite Version 2 that was provided by Steve, KF7O. +Do not use this version unless you have HL2 hardware. It will produce error messages if used with HL1. + +Quisk Version 4.1.3 December 2016 +================================== +The VNA program quisk_vna.py now works with either the Hermes hardware or the HiQSDR hardware. The +quisk_vna.py program is part of Quisk. You need Hermes-Lite firmware 32 or above to use VNA. + +I added some defaults for the 137k and 500k bands. These bands are experimental, so be sure +to set the lower and upper edge on the Config/Radio/Bands screen and follow the rules for your country. + +If you set the TX Level for a band to zero, the PTT button is grayed out for that band. This +is a visual reminder not to transmit on that band. + +Quisk Version 4.1.2 October 2016 +================================= +This contains a new version of the Quisk Vector Network Analyzer program quisk_vna.py. +The VNA program works with the Quisk hardware and the HiQSDR. I replaced the correction +logic, added a new Calibrate screen and improved the formatting. + +I added a feature to change the Quisk waterfall colors on the Fonts configuration screen. +The new colors were provided by David Fainitski. + +Quisk Version 4.1.1 June 2016 +============================== +Quisk can now display and tune multiple sub-receivers if your hardware has them. It currently +works with the Hermes-Lite hardware. Push the Help button for documentation. You can tune +sub-receivers to different bands and modes, and play audio from any of them. + +You can select different color schemes from the Radio/Font configuration screen. + +I added code to speed up the display of configuration pages when the Config button is pushed. + +For users of the HiQSDR, I added sample rates of 1536 and 1920 ksps. These are 16-bit samples, +and require recent DL2STG firmware. I changed the VNA program shutdown messages. + +Quisk Version 4.0.5 April 2016 +=============================== +I added a new column to the Bands screen for transverter offset. I added the favorites file path +to the Options radio screen. If you leave this blank, the default is used. I fixed a bug that +caused a difference in drawing the graph screen between wx 2.8 and wx 3.0. I added additional +sample rates 1536 and 1920 ksps. + +Quisk Version 4.0.4 February 2016 +================================== +For radios using UDP (HiQSDR, Hermes, etc.) I added the transmit IP and port number to the +radio configuration screens in order to support multiple radios on the same network. Leave +these blank for normal operation. I fixed a drawing error that seems to occur on wx 3.0. + +Quisk Version 4.0.3 December 2015 +================================== +This is mostly a bugfix release. It changes Unicode JSON strings to ASCII. +It adds additional PulseAudio features provided by Eric Thornton. Thanks Eric! + +Quisk Version 4.0.2 December 2015 +================================== +This is mostly a bugfix release. It fixes problems with the DX cluster telnet server. +It adds additional PulseAudio features provided by Eric Thornton. Thanks Eric! + +Quisk Version 4.0.1 November 2015 +================================== +This is mostly a bugfix release. It also adds the "Odyssey" radio by David Fainitski. + +Quisk Version 4.0.0 November 2015 +================================== +This is a major new release of Quisk. Comments, suggestions and bug reports are welcome. +There is a lot of new code here, but remember that the prior version is still on my web page. + +Quisk now has two new screen layouts. The "large screen" layout is the default, and is designed for +PCs. It uses the full screen width in order to show as wide a graph as possible, and to make +mouse tuning easy. The new layout "small screen" is designed for small screens such as touch screens +used for single-board computers. But "small screen" can also be used by those with sight +impairment, or by those who run Quisk at narrow widths on a PC. It has more button rows; +and the band, mode and screen select buttons are hidden behind three master buttons. To select +the band, for example, you press the band master button, and a list of bands pops up; then select +the band. Press the Help button to read the new help file. Set button_layout = "Small screen" +in your config file; but see below. + +If you have written your own quisk_widgets.py file to add custom widgets to the bottom of your +screen, you will need to change this file to accommodate both layouts. See n2adr/quisk_widgets.py +and hermes/quisk_widgets.py for examples. + +Quisk now has configuration screens to display and edit its settings. For most users, this makes +config files obsolete. Press the Config button and see the additional tabs, and be sure to read the +config help tab. Furthermore, Quisk can save different settings for different radios. For example, +you can have a block of settings named HiQSDR and a separate block named SoftRock. When Quisk starts, +it can ask which radio you want to use. This feature should appeal to those who have trouble dealing +with config files, and to advanced users with multiple radios. + +If you do nothing, Quisk operates as before, and the settings feature does nothing. You have to +turn it on by making a named block of settings; a "radio". If you make some changes that +cause Quisk to fail to start, just start Quisk with the "-a" or "--ask" option, and specify +"ConfigFileRadio" as the startup radio. The "ConfigFileRadio" is the radio as specified in the +config file, and no internal settings are used. This should not happen, but the code is very new, and +I expect that it needs more work. + +The settings screens take the place of config files, although config files can still be used. +After editing settings, it is necessary to restart Quisk to make the settings happen. Press the +"Restart" button. Some changes will always require a restart, such as the button layout. But I +expect to make many settings happen instantly without a restart. + +Quisk Version 3.7.8 November 2015 +================================== +Foreign users can have trouble with their config directory on Windows because the "Documents" +directory is in a foreign language; for example, "Mine Dokumenter". I changed Quisk to use +the registry to find the name of the "Documents" directory so Quisk should find it in any language. + +When playing a CQ loop using the "Transmit sound from WAV file" feature, pressing +hardware PTT will cancel the play loop. + +I added the hardware information to the top of the screen. This is convenient when running +multiple instances of Quisk with multiple hardwares. I removed the SoftRock status line at +the bottom of the screen. The same information is in the Config/Config screen and in the +title line at the top. + +When operating in Split mode, the RIT control used to only change the Rx channel. But +that meant that the other Tx channel did not have RIT. So in CW mode, a station exactly +on the Tx frequency would not be heard. RIT now works on both channels in Split mode. + +The "Record Rx audio" feature now records Tx audio too. This is useful to record and review +a QSO. + +Quisk Version 3.7.7 September 2015 +=================================== +Eric and I fixed some problems with the PulseAudio code, including problems displaying very +long device names. I reorganized the file quisk_conf_defaults.py to make it easier +to read. I changed the colors on the Config screen to make the controls look better. + +Quisk Version 3.7.6 September 2015 +=================================== +The PulseAudio code was completely rewritten by Eric Thornton, KM4DSJ. It now uses +the asynchronous interface. This will: +1)Improve stability/performance. We can now manage latency events how we want to and prevent +ever increasing latency due to CPU load or network delays. latency_millisecs in the quick_conf.py +file will established the target buffer size for playback. Realtime latency for pulse audio +streams is available on the config/status tab. + +2)Allow connection to two different PulseAudio servers. The default connection is to the local +machine for normal usage. A separate machine can be specified to pass IQ audio via the network. +This is useful to utilize a Softrock or Peaberry via a raspberry pi or other remote computer. +Specify IQ_Server_IP = "your.ip.here" in your quisk_conf.py to utilize this option. Be sure to +set latency_millisecs to something reasonable (300 works for me over wifi). NOTE: To keep network +congestion down, all modes except CW will "cork" the idle up/down IQ stream. +3)Quisk will attempt to utilize native 16 bit LE format if a PulseAudio sink is configured this way. +Default fallback is Float 32. This reduces network overhead for remote IQ audio streaming while not +losing resolution for any local streams that are configured as floating point. + +Additional changes: +-Remote control of Softrock receivers via usbsoftrock. Add usbsr_ip_address = "your.ip.here", +usbsr_port=port#, and import hardware_net.py in quick_conf.py to communicate with a usbsoftrock +daemon. Start usbsoftrock on the remote machine with the -D option. +-Added a macports target to the makefile to utilize dependency libraries from macports. +This option now allows use of PulseAudio on OSX. + +Eric +KM4DSJ + + +Quisk Version 3.7.5 September 2015 +=================================== +I changed the code for full-screen mode. You can now enter window sizes directly. See +window_width in the file quisk_conf_defaults.py. + +I added two more bits to tx_control for the FPGA protocol. I updated the PyUSB +package that ships with Quisk to version 1.0.0b2. + +The received characters from FreeDV now appear in the upper left of the graph. I added +the new FreeDV 700 and 700B modes. Right-click the FDV button to select the mode. + +To make it clear when buttons have added functions, I added a miniature slider to buttons +that can be right-clicked, and a circular arrow to buttons that can be clicked repeatedly. +This can be turned off in your config file. + +Quisk Version 3.7.4 August 2015 +================================ +The Spot button transmits a variable level carrier in all modes, not just SSB. The new config +file option "spot_button_keys_tx = True" will cause the Spot button to key the transmitter. The +default is False. The up button for the Favorites screen used to set the frequency in Hertz, +but the frequencies must be in Megahertz. The ctcss was also broken. This has been fixed. + +I added a new Hermes-Lite model configuration file hermes/quisk_conf2.py. This is for +use by people who can program in Python and want to add features to Quisk. It is set up +to send the Spot button status on the J16 connector. Quisk now operates in CW mode using +the new Hermes-Lite key input. For other modes, use the PTT button. On the Hermes, +use the PTT button with Spot, even for CW. The CW key always transmits full power CW even +if the mode is not CW. + +There is now an option to receive microphone samples from a UDP device. This is used for +SDR hardware that has a built-in codec. See quisk_conf_defaults.py. + +The FreeDV code was completely rewritten. It is now simpler, and it is easier to change to a +newer libcodec2. See the file freedvpkg/README.txt. If anyone is running on Apple OSX, and +has problems, email me, and look at freedv.c. + +The config file option graph_width=0.8 sets the width of the graph. If this is set to exactly 1.0, +Quisk will run in full-screen mode. This is meant for built-in screens, tablets etc. that lack +window management. + +Quisk Version 3.7.2 July 2015 +============================== +I made some changes for Hermes-Lite. You must add rx_udp_clock to your config file. You can adjust +rx_udp_clock slightly to correct the frequency display. See hermes/quisk_conf.py. +I added Hermes_BandDict to the config file. It controls the bits on the J16 connector of the Hermes-Lite. + +Quisk will now keep the same filter for a given mode when changing bands. I added RepeaterOffset() to +the softrock hardware file so that repeater offsets work. I added mode DGT-FM for digital FM modes. + + +Quisk Version 3.7.1 July 2015 +============================== +This is a bug fix release to correct an incompatibility between my FPGA firmware and that of +Stephen, DL2STG. He was using the byte in the control record that I used to send the sidetone volume. +I now send the sidetone volume as byte 17. + +I added the config file parameter use_sidetone to add/remove the Sto sidetone volume control. It +used to be necessary to use the hardware file for this. The default is no sidetone. To add the +control, add "use_sidetone=1" to your config file. + +I fixed a bug in the Audio Plackback button and Hermes-Lite. + +Quisk Version 3.7.0 July 2015 +============================== +Quisk now supports a new radio, the HermesLite. If you have a HermesLite, copy hermes/quisk_conf.py +to your config file and modify the copy. You should only need to change the soundcard names. +If you use Quisk on multiple radios, remember that you can have multiple config files. +Use "python quisk.py -c my_config_file.py" or its equivalent to choose among config files. This +code is currently under test and may have problems. + +The favorites screen now has two more columns for repeater offset and CTCSS tone. See the file +quisk_conf_defaults.py. You must set do_repeater_offset=True, and your hardware must be capable +of performing the shift. + +The FDV feature is now in "final" form. Please see freedvpkg/README.txt. Note that FDV is +a moving target. + +There is now an option to send radio sound to a UDP device. This is used to send radio sound to +SDR hardware that has a built-in codec. See quisk_conf_defaults.py. + +The SoftRock hardware file now supports the key line as a PTT button for voice modes. Use this if you +have a hardware mic button that you can connect to the key line. + +I added the sidetone volume to the HiQSDR control data to accommodate new hardware. I softened the +volume controls Vol and Sto for a smoother response. I added the new config file option cw_delay. + +Quisk Version 3.6.22 April 2015 +=============================== +FreeDV is the combination of the codec2 codec and the fdmdv modem. It provides digital voice +in 1200 Hz bandwidth suitable for HF transmission. You can use FreeDV by downloading the FreeDv +program from freedv.org, and connecting it to Quisk using the usual digital mode DGT-U. But now +there is a simpler way. Quisk has a new mode button FDV. Just push the FDV mode and talk. See +http://www.rowetel.com/blog/?page_id=452. + +Quisk will add the FDV mode button unless your config file contains the line + add_freedv_button = 0 +If there is a problem with the freedv module, the button will be grayed out. See README.txt in the +freedvpkg subdirectory for more information. The "Split" button currently fails with FDV. + +With the mouse in the frequency display, roll the mouse wheel to change the digit. + +The new config option hamlib_ip specifies the Hamlib IP address; default "localhost". + +The IMD button now has a right-click level control like the Spot button. This is more +convenient than having a few fixed levels. + +The Spot button can now transmit a carrier at zero amplitude. This is useful for testing +the output noise. If you wrote your own OnSpot() method, level -1 is now Spot button off, +and the level is now 0 to 1000. + +Quisk Version 3.6.21 March 2015 +=============================== +Quisk can now transmit a message from a WAV file. Record your message at +a high level (near clipping) at 48 ksps, 16-bit, one channel (monophonic). +Then enter the file name on the Config/Config screen. Press the "File play" +button to transmit. Quisk will press the PTT button for you, and release it +during pauses. To interrupt playback, press PTT or release FilePlay so you can answer. + +The "Split" button has been replaced with a "Splt" button and a "Rev" button. The "Splt" +button splits Rx and Tx; and if you click it with the right mouse button instead of the +left, it also locks the Tx frequency so tuning changes the Rx frequency. The "Rev" +button reverses the Tx and Rx frequencies. These features were suggested by Mario, DH5YM. + +I added a new parameter mic_agc_level to the config file to control the mic AGC. Input levels +below mic_agc_level are ignored. The default is 0.1. Increase this (up to 1.0) to reduce the +AGC range, and reduce it to increase the AGC mic gain boost. + +Philip Lee contributed a patch to the PulseAudio code to set the play buffer size. + +Quisk Version 3.6.20 December 2014 +================================== +Thanks to Graeme, ZL2APV, quisk now changes both channels of a control when using mixer_settings[]. +He reports that setting boolean values does not work, but it works on my machine. More testing +is needed. + +The new config file dictionary bandTransverterOffset[] gives the offset in Hertz for bands that are +used with a transverter; for example 144000000 - 28000000 for a two meter transverter. This +currently works for HiQSDR, SoftRock and SdrIQ. I also changed the graph X axis code so gigahertz +frequency labels are not too wide. + +Quisk Version 3.6.19 October 2014 +================================= +Mario, DL3LSM, contributed changes for MacOS support. Thanks! + +I added device names to PulseAudio. The PulseAudio name "pulse" still refers to the default +device. Otherwise, enter a PulseAudio name such as "pulse:alsa_input.pci-0000_00_1b.0.analog-stereo". +See quisk_conf_defaults.py or docs.html. PulseAudio support enables you to connect to recent +versions of wsjt-x. To turn this off, set show_pulse_audio_devices = False in your config file. + +Quisk Version 3.6.18 June 2014 +============================== +The Windows installer now works with the new WxPython 3.0. Previously it required 2.x. +I fixed an annoying sound loop that happened on Windows when quitting Quisk. + +The Rx Filter screen now displays the bandwidth of the filter at the 3 and 6 dB points. +I improved the calculation of FFT size for fft_size_multiplier==0. I added more FFT buffers +to improve performance at high data rates. + +When you right-click the S-meter, there are new options to change the S-meter time +constant, and to measure the audio voltage. + +Quisk Version 3.6.17 June 2014 +============================== +Philip G. Lee contributed code to provide native PulseAudio support for Quisk. You will need +to install the package "libpulse-dev" to compile. Thanks Philip! + +Stephen, K6BSD, provided patches to support FreeBSD. Thanks Stephen! + +The Spot button now works in CW mode with softrock USB hardware. There is a new +slider control for SoftRock transmit level on the Config/Config screen. + +The new config file options file_name_audio and file_name_samples are the initial names +for saving audio and samples on the Config/Config screen. +I added more sample rates to support different hardware. + +Quisk Version 3.6.16 March 2014 +=============================== +I re-wrote the CW transmit logic for SoftRock transmitters. It now implements semi break-in +CW operation. The config file options are key_poll_msec and key_hang_time. + +I added the config file option sample_playback_name to play the raw samples to a loopback or +VAC device. You can access the raw samples by reading them back with another program. +See quisk_conf_defaults.py. The sample rate is the same as the hardware sample rate. + +Quisk Version 3.6.14 November 2013 +================================== +I restored audio to the "Tx Audio" screen. I increased the digital (DGT-U etc.) transmit +bandwidth to 5 kHz. The Ys and Yz settings that control the waterfall colors are now +saved for each band. Just adjust Ys and Yz on a band, and these settings are restored when +you return to the band. + +I improved the receive AGC to reduce distortion. The AGC button now has separate slider values +for On and Off. Right click the AGC button to set the sliders. Push the "Help" button to read +the description of the AGC. + +I corrected some bugs related to dark color schemes, and added color_bg_txt to control the +slider text on the main screen. + +Quisk Version 3.6.13 October 2013 +================================= +I added VOX, voice operated relay. I improved the transmit audio speech processing. I +added controls for the audio clip and preemphasis levels to the Tx Audio tab of the Config +screen. Press the "Help" button to read the documentation on how to set your audio levels. + +Quisk Version 3.6.12 August 2013 +================================ +I fixed a bug that produced slight distortion in the transmitted signal. I added the Enum +type to the mixer control "mixer_settings[]". Be sure to use a Python float if you want a +decimal fraction 0.0 to 1.0; that is, use "1.0" for 100%, not "1". + +The config file now defines hot keys that will press the PTT button when the keys are pressed. + +I added a new tab to the Config Screen for testing transmit audio. You can record and play +back the processed microphone output to hear what it sounds like. I plan to add audio +controls to this screen in a future release. I think the record/playback is more useful +than a simple loopback because I find it difficult to talk and listen at the same time. + +Quisk Version 3.6.11 July 2013 +============================== +I increased the size of the SoftRock status line. + +You can now change frequency by left clicking on the digits in the frequency display. Click on +the upper part of a digit to increase it, and the lower part to decrease it. + +I added some GREAT features from Christof, DJ4CM: + Symbols for buttons. But you can change back to text with use_unicode_symbols = False. + Two new buttons for direct entry and recall of items on the favorites screen. + A new window to display saved stations, favorites and dx cluster data below the frequency axis. + A feature to query a dx cluster using telnet, and display the stations. +The symbols use Unicode, and were tested on Linux and Windows, and on computers in Germany and the USA. +But if your Windows does not support Unicode, you will need to add use_unicode_symbols = False. +Read quisk_conf_defaults.py (as usual), and look for quisk_typeface, btn_text_, use_unicode_symbols, +sym_stat_, station_display_lines, and dxClHost to see how to use these features by Christof. + +Quisk Version 3.6.10 May 2013 +============================= +I made some changes to my n2adr hardware and widget files. I increased the timeout for select() +in the UDP code. I added a patch from David Turnbull, AE9RB, to make Quisk USB control work with +the Peaberry. The problem was discovered and solved by ON7VQ. + +Windows changes the Documents folder name for different languages, and has no standard way to +find it. I added "Mine Dokumenter" to the list. + +The AGC release time and mic gain control are now parameters in the config file. + +I added some features from Christof, DJ4CM: additional color control, double click tune on favorites +screen, and format changes. + +Quisk Version 3.6.9 April 2013 +============================== +The new config file parameter digital_output_level controls the sound level to Fldigi, etc. +I fixed some small bugs in the Windows DirectX sound system. + +When using the Split feature, Quisk can now receive on both the Rx and the Tx frequencies, and play them +in stereo. When working DX, you can now hear the DX in one ear and the pileup in the other. I like +the lower frequency signal to be left, and the higher one right. This is controlled by the new config +file option split_rxtx that can be 1, 2, 3 or 4. See the file quisk_conf_defaults.py (as usual). + +This version of Quisk supports the new features in my HiQSDR FPGA firmware version 1.4. If you are +using Quisk to set your hardware IP to rx_udp_ip, please be sure that rx_udp_ip_netmask is correct. +The default is 255.255.255.0. + +Quisk Version 3.6.8 March 2013 +============================== +The new config file parameter button_font_size can be changed to reduce the size of the button font. +This is useful for a netbook when the lower screen resolution results in crowded buttons. + +There is a new button "Notch" for an automatic notch filter to get rid of carriers in SSB signals. It +works in CW too, but is less useful because the sharp filters can get rid of unwanted signals. The "Notch" +is an advanced feature based on the FFT and not the LMS algorithm. + +Quisk Version 3.6.7 February 2013 +================================= +I added a check for the correct wx version. Thanks to Mario, DH5YM. Selecting "Config/Favorites/TuneTo" +now changes to the configured default_screen instead of the waterfall screen. Thanks to Detlef, DL7IY. +The mute button and volume now controls only the radio sound, and not the digital output. Thanks to Mario, DH5YM. + +Previously, Fldigi XML-RPC control only worked if a digital mode was selected and a digital audio device +was specified. Now it is always active unless you turn it off in the config file. + +The transmit power level and the digital transmit power level can now be adjusted on the config screen. The +levels are a percentage of tx_level as set in the config file. The meaning of digital_tx_level was changed. +It is now the maximum percentage value for the slider. Thanks to Hubert, DG7MGY. + +There is a new feature to save frequencies and return to them. When you have tuned in a signal of interest, +press the "Save" button to save the frequency, band and mode. Repeat for more signals. Now press "Next", +to switch to the next saved signal, and press "Next" repeatedly to cycle through the list. To delete a +saved signal, first tune to it with "Next" and then press "Delete". If you save a large number of signals, +right click the "Next" button, and you will get a popup menu so you can jump directly to a station. Thanks +to Detlef, DL7IY. + +I had to renumber the columns for the Quisk buttons. That won't matter to you unless you add your own widgets +to the bottom of the screen by importing your own quisk_widgets. Then you will need to renumber your columns. +See n2adr/quisk_widgets.py for an example. + +Quisk Version 3.6.6 January 2013 +================================ +I corrected the frequency measurement feature so it works with RIT. This feature was successfully +verified in the November 2012 ARRL Frequency Measurement Test. I fixed a bug in Fldigi frequency +control thanks to Hubert, DG7MGY. + +The config file has a new parameter mouse_wheelmod to control the mouse wheel +sensitivity. Thanks to DG7MGY for the patch. + +I changed the digital filters so they have a bandwidth up to 20 ksps. This accommodates more digital modes. +There are now three digital modes: DGT-U and DGT-L decode the audio as upper or lower sideband. The old +DGTL mode is now DGT-U. The mode DGT-IQ does not decode. It sends the I/Q samples directly to the +sound card device. It is interesting to listen to the DGT-IQ signal, as it provides binaural reception. +Also, the PTT can be controlled by either Quisk or Fldigi, not just by Quisk. + +There is a new tab on the Config screen where you can enter the frequencies and modes for favorite +stations. This can be used to list and tune to repeaters or nets. The page works like a spreadsheet. +Fill in the rows with an arbitrary name, the frequency as integer Hertz or decimal megahertz, the mode and +an arbitrary description. Right click on the left row label for a popup menu that allows you tune to that +row. Thanks to Brian KF7WPK for suggesting this feature. + +Quisk Version 3.6.5 November 2012 +================================= +I added a waterfall method ChangeRfGain() that enables you to keep the waterfall colors constant +for changes in the RF gain control. See my n2adr directory for an example of how to use it. + +I added the ability to connect Quisk to the hamlib rigctld daemon. This enables Quisk to work +with any rig compatible with hamlib. For an example, see quisk_conf_kx3.py. Push the Help button +for documentation. + +I added a feature to measure the frequency of a continuous RF signal. Right click the S-meter window +to turn it on. Push the Help button for documentation. It is meant for precise frequency measurement +such as would be needed to characterize crystals for a filter. Precision is 0.01 Hertz. + +Quisk Version 3.6.4 September 2012 +================================== +I added Hamlib control to Quisk. Set your digital or logging program to rig2, +device localhost:4575. See the Help and docs.html. This is used to control Quisk +from other digital mode programs such as WSPR. + +I added the Y scale to the graph above the waterfall. + +Quisk can now record the speaker audio and the digital samples to a WAV file. Set the +file names using the config screen, and then use the "FileRec" button to start recording. +Press the Help button for more information. + +Quisk Version 3.6.3 July 2012 +============================= +Thanks to Steve Murphy, KB8RWQ for the patch adding additional color control, and for his +dark color design. + +I am using Quisk with my AR8600 receiver 10.7 MHz IF output as a general coverage receiver. +My config file is n2adr/quisk_conf_8600.py. This covers the VHF and UHF bands, and so +I needed to add some FM repeater and scanner features. I added a Squelch button for FM. +Right-click the button to adjust the squelch level. The Squech and AGC buttons +are combined to save space. The new configuration file items freq_spacing and +freq_base are used to round frequencies to channel spacings on VHF. There is scanner +logic in my config file. You should look at this if you use Quisk with a transverter for +the higher bands. With my hardware it is able to scan known repeater frequencies jumping +across bands as it scans. The 960 ksps rate of Quisk and HiQSDR is very useful at VHF +and higher. + +I added tabs to the config screen, and cleaned it up. + +I added a record and playback button. Press Record to start a new recording of radio +sound. The maximum recording length is set in the config file, and the default is 15 +seconds. After this limit, the most recent 15 seconds of sound is retained. To play +the recorded sound, press the Play button. If you are transmitting, the recorded sound +is transmitted provided the microphone and playback sample rate are both 48000 sps. The +transmitted recorded sound is not subjected to the usual audio processing. That means +that you can play another ham's audio back and give him/her a good idea of how it sounds. + +Quisk Version 3.6.2 May 2012 +============================ +I added a display of the filter bandwidth to the graph screen. This is based on code +provided by Terry Fox, WB4JFI. Thanks Terry! See the file quisk_conf_defaults.py. + +I added detailed information on each sound device to the config screen. The Test +button now generates AM and FM as well as CW and SSB. + +The receive filtering has been re-written to improve the shape of the filters and to +reduce the CPU time. Quisk now runs on my fan-less Shuttle Atom machine at speeds +up to 480 ksps. The CW filters are particularly nice. + +Quisk Version 3.6.1 April 2012 +============================== +There is a new "DGTL" mode to send Quisk audio to an external digital mode program +such as Fldigi. Read the file quisk_conf_defaults.py to see the new config file +options available. Use the Help button for basic information, and see docs.html. + +I changed the 60 meter operation to agree with new FCC rules (for the USA). See +the configuration file for items to control 60 meters. + +Quisk Version 3.6.0 March 2012 +============================== +There are no new user features in this release, and no changes to the HiQSDR code. +This version adds a new feature for those writing C-language extension modules +that need to access C code from the _quisk extension module. Examples are the +SDR-IQ and the Charleston extension modules. This feature was requested by +Maitland Bottoms, AA4HS, and he also provided patches. + +Previously, symbols from the _quisk module were linked to sub-modules with the +C linker. Now _quisk exports symbols using the Python CObject or Capsule +interface. The documentation is in import_quisk_api.c. Only minimal changes +to extension modules are required, as most changes are in _quisk. The linker +method still works on Linux, but the new interface is highly recommended. + +Quisk Version 3.5.12 February 2012 +================================== +There are no changes to Quisk, but this version includes the new Quisk VNA program +that enables you to use my original transceiver hardware and the newer HiQSDR +hardware as a vector network analyzer. Use "python quisk_vna.py" to run it. + +Quisk Version 3.5.11 December 2011 +================================== +I fixed a bug that caused the microphone to freeze when sending the mic sound to +the SoftRock for transmit. + +Quisk Version 3.5.10 December 2011 +================================== +Lucian Langa contributed a patch to return the primary display size for dual +displays. If you decreased graph_width for dual displays you will need to change it +back to 0.80 (or similar). + +I improved the transmit audio filters to reduce spurs and decrease processing time. +The mic sample rate can now be either 48000 (as before) or 8000 samples per second. +The plan is to make Quisk run effectively on small laptops or even tablet computers. +Remember to adjust mic_clip and mic_preemphasis in your config file. + +Quisk Version 3.5.9 November 2011 +================================= +I fixed a bug in Windows that occurs only when using the mic for transmit. + +Quisk Version 3.5.8 October 2011 +================================ +The Windows version is now equal to the Linux version, and transmits properly. + +I added a parameter agc_off_gain to the config file. It controls the audio gain +when AGC is off. Reduce it if sound with AGC off is too loud. Note that even with +AGC off, the output is limited to the clip level. I made some other improvements to AGC. + +I added FM transmit. The modulation index can be set in your config file. + +I made some improvements to demodulation, and to the SSB transmit filter. I fixed an +array out of bounds bug in transmit. + +Quisk Version 3.5.7 September 2011 +================================== +This is a quick release to fix two bugs in 3.5.6, the message "expected integer" and +faint audio for FM. I also added a new parameter agc_max_gain to the default +configuration file to control the scale of the AGC slider. + +Quisk Version 3.5.6 September 2011 +================================== +The Spot button now has a level adjustment instead of fixed values. Right-click the +button to adjust. There are now three buttons with a slider adjustment, namely AGC, +Spot and the right-most filter button. + +I added a feature to measure and remove any DC component in the UDP samples. I fixed a +problem with the waterfall display when zoomed and using band up-down. + +There is a new adjustable AGC control. Right click it to show the slider adjustment. The +full up position corresponds to the old AGC 1. + +I removed the 1650 Hertz offset when transmitting SSB. It was not necessary and cluttered the code. + +Quisk Version 3.5.5 July 2011 +============================= +These changes only affect the N2ADR 2010 transceiver and the improved version, the HiQSDR. + +I moved all the files into a new package directory hiqsdr. The old n2adr directory +has only the special files I use at my shack. Please change your config file as follows: + +from hiqsdr import quisk_hardware # Special hardware file +use_rx_udp = 1 # Use this for the N2ADR-2010 +use_rx_udp = 2 # Use this for the HiQSDR + +The sample config file quisk_conf.py in hiqsdr can be used as is for the HiQSDR. +There is a new dictionary tx_level in the config file to set the transmit level. See +quisk_conf_defaults.py for other features that can be set for the HiQSDR. + +There is a new FPGA firmware version 1.1 available to support the new HiQSDR features. +Note that your firmware version is shown on the Config screen. It is not necessary to +update your firmware unless you use the HiQSDR and you want the new HiQSDR control lines +to work. If you do update your firmware, you must run Quisk 3.5.5 or later. + +Quisk Version 3.5.4 June 2011 +============================= +I added another slider labeled "Zo" to zoom (expand) the graph screen scale +so that narrow signals can be examined. The center of the graph is changed +to the tuning frequency when zoom is turned on. To cancel zoom, move the +slider back to the bottom position. You can tune as usual even if zoom is on. + +I put the band buttons on one line so I could add more control buttons. + +Quisk Version 3.5.3 May 2011 +============================ +I added "Documents" as a possible config file location (for Windows 7). + +These changes are specific to my 2009/2010 transceiver hardware: +I now detect and display the firmware version. The files conf_transceiver.py +and hardware_transceiver.py are now the basic config and hardware files for my +transceiver. The spot button now appears without a special widgets file, so no +widgets file is necessary. The file quisk_hardware.py is still the hardware file +used in my station, but it is mostly useful as an example of what is possible, +not as a starting point for use by others. + + +Quisk Version 3.5.2 April 2011 +============================== +I added code from Ethan Blanton, KB8OJH, to provide direct frequency control +of the Si570 chip in many SoftRocks. I added AM transmit and improved AM +receive. I added FM de-emphasis to receive. I added a noise blanker. + +It is now possible to delay samples (tx_channel_delay) and correct the amplitude +and phase for the sound card play device (SoftRock transmit). Unfortunately +receive sound card corrections will need to be re-entered. + +The filter bandwidths for each mode can now be set in the config file. And +you can right-click the right-most filter button to adjust its bandwidth. + +Quisk Version 3.5.1 February 2011 +================================= +The phase correction control has been improved to allow multiple correction +points per band. Unfortunately this will require re-entering corrections. + +I added mic_preemphasis and mic_clip to the config file to control Tx audio processing. + +Quisk Version 3.5.0 January 2011 +================================ +Starting with this version, a Windows version of Quisk is available (alpha code). +I changed the amplitude/phase correction control, and added config file +options (rx_max_phase_correct) to control the maximum available correction. + +Quisk Version 3.4.14 January 2011 +================================= +The "alsa:" names can now be used for mixer settings. I added simplified +config and hardware files for my 2010 transceiver hardware. I moved the one +sample delay for some sound cards into the config file instead of using the +#define FIX_H101 (which remains for backward compatibility). I added more +buttons "GraphP2" to the Graph button to activate a peak hold function. There +are config file options graph_peak_hold_1 and _2 to control the time constant. + +Quisk Version 3.4.13 December 2010 +================================== +I decreased the microphone speech processing preemphasis and clipping. +I added a config parameter key_poll_msec to control the SoftRock USB +poll for key status. I improved the config screen. Alsa names can +now be strings like "alsa:NVidia" that match the card/device info. +Thanks to Joachim Schneider, DB6QS, I made some improvements to SoftRock +USB control. + +Quisk Version 3.4.11 November 2010 +=============================== +Thanks to Sid Boyce, G3VBV, for sending me SoftRock hardware to work with. +The "mic_play" logic was re-written so that transmit I/Q samples can be +sent from a sound card to hardware that uses QSD up-conversion. I added +USB access through pyusb to control recent SoftRock models. A new package +"softrock" directly supports several SoftRock models. + +Change Spot Button to transmit at carrier frequency. + +Add a Split button to enable split receive and transmit frequencies. + +Fix band change data for pan adapter users. + +Try to make easy_install work better. + +Quisk Version 3.4.8 August 2010 +=============================== +A new config file option "playback_rate" can set the radio sound play rate. + +I added a button to the config screen to change the decimation rate for +hardware that supports this. See the new "VarDecim" methods in +quisk_hardware_model.py. I added this feature to the SDR-IQ hardware +file sdriqpkg/quisk_hardware.py, and to n2adr/quisk_hardware.py. + +Thanks to John Nogatch AC6SL for a bug fix. + +Quisk Version 3.4.6 July 2010 +============================= +I improved the mouse tuning by eliminating a tendency to tune backward. + +I made the sdriq extension and my n2adr code into packages in the directories +"sdriqpkg" and "n2adr". The new package architecture will make it easier for +authors to write Quisk extensions. See the example config files quisk_conf_sdriq.py +and quisk_conf_n2adr.py to see how to change your imports: + + from sdriqpkg import sdriq + from sdriqpkg import quisk_hardware + from n2adr import quisk_hardware + from n2adr import quisk_widgets + +Thanks to Terry Fox, WB4JFI, for improvements to Quisk: + Code to support the Charleston hardware (libusb-dev required). + Code to add a third FFT data block. + +Quisk Version 3.4.3 June 2010 +============================= +The hardware open() method now returns a string for the config screen. If +you have a custom hardware file, create a string or return the base class string. + +I made the SDR-IQ code into a separate Python extension module "sdriq". +This module can serve as a model for other hardware extensions. It is +the model for the Charleston hardware extension module. The sdriq.so +file needs _quisk.so, so put both in the same directory. + +I corrected the decimation for sample rates greater than 240 ksps, and +improved the filters for all decimations to reduce "images". + +The following changes are only relevant if you use the SDR-IQ for capture: + + You need to add these lines to your config file (see quisk_hardware_defaults.py): + import quisk_hardware_sdriq as quisk_hardware + display_fraction = 0.85 + There is now a special hardware file for the SDR-IQ. If you have a + custom hardware file that uses the SDR-IQ you need to use + quisk_hardware_sdriq as its base class (instead of quisk_hardware_model). + +[See version 3.4.5 for further SDR-IQ changes] + +Quisk Version 3.4.2 May 2010 +============================ +The config file has a new option to add an external demodulation module. I +I added the ability to play in stereo, and corrected the sidetone logic. + +The config file has a new option to add a full duplex button. + +I added the ability to use PortAudio for sound card access. PortAudio +can also be used to connect Quisk to other programs. + +I added a key up delay to the is_key_down() serial port code and fixed a +sound card CW bug. + +A new config file entry can make amplitude/phase corrections independent +of band. This is needed for a panadapter. + +I fixed the compressed graph labels at high sample rates. + +Quisk Version 3.3.7 April 2010 +================================= +If you get samples from a UDP port, you can specify the decimation rate in the +config file. + +If you send samples to a sound card for transmit, CW now works (as does SSB). + +Quisk Version 3.3.6 February 2010 +================================= +I added BandEdge to the config file, and added code to Quisk to make the +frequency and band changes more rational. I changed the config file +attribute freqTime to bandTime (see changes). + +You can now define a class named "Hardware" in your config file, and then +you don't need a separate hardware file. This is only recommended for +simple hardware needs. See docs.html. + +If you use the microphone and send samples with UDP, the audio is now +centered at 1650 Hertz, and you must add/subtract this offset when +setting the transmit frequency. + +A number of valuable patches were submitted by Andrew Nilsson, VK6JBL, +and these were incorporated into Quisk: + + The band buttons displayed can be changed in the config file (bandLabels). + + The 6 meter band was added (change bandLabels to show it). + + Turn on add_imd_button in the config file to generate 2-tone test signals. + + The two new functions QS.capt_channels(i, q) and QS.play_channels(i, q) + will set the capture and playback channel numbers at any time. + + If you set the key method to "", the new function QS.set_key_down(1) will + set the key state up or down. This enables you change the key state using + either C or Python; for example, to add a "MOX" button. + + The microphone samples can now be output to a sound card for transmit. See + the additional items in the config file. + + The new config file parameter mouse_tune_method causes mouse drag tuning + to change the VFO frequency, not the Quisk tuning frequency. + +I moved microphone_name and tx_ip (for the microphone) to the config +file from the hardware file so that all the mic parameters are together. + +Quisk Version 3.3.1 December 2009 +================================= + +For sound card input, I added controls to correct amplitude and phase +balance. Press the new button on the config screen. A different +correction is saved for each band. See the help file. + +I added the new band "Audio". It sets the VFO frequency to +zero and is meant to be used with a sound card. I changed the WWV and CHU +bands to a new Time band. The time frequencies are named freqTime and can +be changed in your config file. + +In the file quisk_conf_defaults.py I changed the default for persistent_state +to True, and added graph_width=0.8 to specify the graph width. + +A period "." in the frequency entry box means megahertz. + +Quisk Version 3.3.0 November 2009 +================================= + +I fixed a bug in the SDR-IQ decimation that produced slight audio +distortion at decimations other than 500. + +If the play device is the null string "", Quisk no longer tunes and +demodulates the signal. This saves CPU cycles when Quisk is used +as a panadapter. + +I added decimation (reduction of sample rate) before the filters so that +Quisk can handle higher sample rates or slower computers. + +I made the waterfall into a splitter window with a graph display at the top. +There are new attributes in the config file to control this feature. + +The numeric value of Ys and Yz are now shown so that the values can be added +to the config file more easily. + +There is a new config option to save the state (band, frequency, etc.) on +exit, and restore it on startup. Only certain bits of state are saved; the +others are still taken from the config file. + +The default config file sets fft_size_multiplier to zero, and this specifies +that Quisk should calculate it for you. + +Quisk Version 3.2.3 September 2009 +================================== + +Fixed a bug that prevented tuning the SDR-IQ when using the +default hardware file. Started adding code to capture sound +from a UDP socket. + +Quisk Version 3.2.2 June 2009 +============================= + +The microphone access was re-written to make it work with more +sound cards. The config file has a new parameter "mic_channel_I" +to specify which sound card channel is used for the mic. + +Added Documentation.html. Fixed lack of poll to ReturnFrequency(). + +Quisk Version 3.2 May 2009 +========================== + +Quisk now uses wxPython instead of Tkinter for its graphical +user interface. You must install the python-wxgtk2.8 package. +Get the latest version available. If you still want to run +the Tkinter version, it is quisk_tk.py. The wxPython version +is much faster. + +Quisk now runs in two threads; a GUI thread and a sound thread. + +I moved the colors to the config file so you can change the +colors more easily. + + +Quisk Version 3.1 April 2009 +============================= + +New hardware file to control the AOR AD8600. + +I added filtering to FM audio to remove CTCSS tones and provide +-6 dB / octave de-emphasis. + +I removed the tkdirect C-language module and replaced it with a +pure Python equivalent. This reduces compilation problems. + +I improved the speed of the screen updates so that Quisk will run +without clicks on slower computers. + + +Quisk Version 3.0.0 April 2009 +=============================== + +Thanks to Leigh L. Klotz, Jr. WA5ZNU, my special hardware control was +removed to separate files so that Quisk now has a cleaner design that +is more useful to others. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..53a890c --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Quisk Software Defined Radio by N2ADR +**December 26, 2023** + +This is Quisk, a Software Defined Radio (SDR). +You supply radio hardware such as Hermes Lite2 or SoftRock to convert +the antenna voltage to I/Q samples. +Then send the samples to a computer running Quisk. +The Quisk software will read the I/Q data, tune it, filter it, +demodulate it, and send the audio to headphones or speakers. +Quisk has a microphone input and a key input so it can operate as a +complete transceiver. + +The web page for this project is [https://groups.io/g/n2adr-sdr](https://groups.io/g/n2adr-sdr). + +The change log is [here](CHANGELOG.txt). + +The documentation is [here](docs.html). + +The help file is [here](help.html). + +This site is under construction. Check back soon. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..ee518f2 --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +#Quisk version 4.2.29 +from .quisk import main diff --git a/__main__.py b/__main__.py new file mode 100644 index 0000000..f4b3ce8 --- /dev/null +++ b/__main__.py @@ -0,0 +1,8 @@ +def main(): + import quisk + if quisk.__file__.find('__init__') >= 0: # quisk is the package + import quisk.quisk as quisk + quisk.main() + +if __name__ == "__main__": + main() diff --git a/ac2yd/Design_2022_0531.pdf b/ac2yd/Design_2022_0531.pdf new file mode 100644 index 0000000..8a54e00 Binary files /dev/null and b/ac2yd/Design_2022_0531.pdf differ diff --git a/ac2yd/__init__.py b/ac2yd/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/ac2yd/__init__.py @@ -0,0 +1 @@ +# diff --git a/ac2yd/control_common.py b/ac2yd/control_common.py new file mode 100644 index 0000000..24d8bdc --- /dev/null +++ b/ac2yd/control_common.py @@ -0,0 +1,408 @@ +# THIS IS THE ENTIRE "RADIO" FOR QUISK RUNNING AS A CONTROL HEAD +# No real radio hardware is attached to the control_head computer. +# +# This software is Copyright (C) 2021-2022 by Ben Cahill and 2006-2022 by James C. Ahlstrom., +# and is licensed for use under the GNU General Public License (GPL). +# See http://www.opensource.org. +# Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!! +# +# This file, control_common.py, along with a radio-specific file, e.g. +# control_softrock.py, control_hermes.py, or similar, allows a radio-less (control_head) Quisk, +# running on this computer, to connect to a remote (remote_radio) instance of Quisk +# that runs on a separate computer. +# +# The remote_radio Quisk controls an attached real radio, and uses the hardware file +# remote_common.py, along with a radio-specific file, e.g. remote_softrock.py, +# remote_hermes.py, or similar, to communicate with this control_head Quisk +# computer via a network connection. +# +# The remote_radio computer should be set up with a static IP address, so that you know +# where to point the control_head. The control head computer may, however, use dynamic +# addressing; the remote radio computer will read the control head address when the +# remote control connection is made. +# +# The main control interface between control_head and remote_radio is via a TCP port; +# this uses very low bandwidth. All functional control, including CW keying, is done via this port. +# +# There are 2 additional ports, both UDP, using low to moderate bandwidth: +# -- Receive graph/waterfall data from the remote_radio +# -- Receive radio sound from the remote_radio and send mic samples +# These use sequential port numbers based on the TCP port number self.remote_ctl_base_port. +# If you need to change the default base port number, you can edit the line in this file +# that looks like (without the #): +# +# self.remote_ctl_base_port = 4585 # Remote Control base TCP port +# self.graph_data_port = self.remote_ctl_base_port + 1 # UDP port for graph data +# self.remote_radio_sound_port = self.remote_ctl_base_port + 2 # UDP port for radio sound and mic +# +# Make sure to edit the corresponding line in remote_common.py to match ports!! +# +# You should be able to use the Quisk control_head along with any/all means of control that +# you normally use to control Quisk, including serial ports, MIDI, and hamlib/rigctl interfaces. +# +# The remote_radio Quisk/computer is assumed to track the local control_head Quisk/computer; +# no attempt is made to verify the remote_radio Quisk's tuning frequency, mode, etc. +# Snap-to Rx tuning for CW works on the control_head Quisk by virtue of graph/waterfall data +# received from the remote_radio Quisk. +# +# To test CW key timing, set DEBUG_CW_SEND_DITS = 1. This issues "perfect" bursts of dits, +# configurable in terms of dit length, dit space, number of dits per burst, and pause between +# bursts (phrases). Search in this file for "DEBUG_CW_SEND_DITS" to find configurable variables. +# To get log output from remote_radio end, in quisk_hardware_remote_radio.py, set DEBUG_CW_JITTER = 1. + +from __future__ import print_function +from __future__ import absolute_import + +DEBUG_CW_JITTER = 0 +DEBUG_CW_SEND_DITS = 0 +DEBUG = 0 + +import socket, time, traceback, hmac, threading, select +import _quisk as QS # Access Quisk C functions via PyMethodDef QuiskMethods[] in quisk.c +import wx + +from quisk_hardware_model import Hardware as BaseHardware + +class ControlCommon(BaseHardware): # This is the Hardware class for the control head + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.app = app # Access Quisk class App (Python) functions + app.remote_control_head = True + self.remote_ctl_base_port = 4585 # Base of ports for remote connection (maybe edit this) + self.remote_ctl_socket = None + self.remote_ctl_connected = False + self.remote_ctl_timestamp = None + self.graph_data_port = self.remote_ctl_base_port + 1 + self.remote_radio_sound_port = self.remote_ctl_base_port + 2 + self.thread_lock = threading.Lock() + self.remote_radio_ip = socket.gethostbyname(self.conf.remote_radio_ip) # Allow either host name or IP address + self.first_heartbeat = True + + self.cw_keydown = 0 + self.cw_phrase_begin_ts = None # timestamp of beginning of cw phrase + self.cw_phrase_end_ts = None + self.cw_phrase_break_duration_secs = 1.0 # cw timestamps will reset to 0 + self.cw_poll_started_ts = None + self.cw_poll_started = False + + if DEBUG_CW_SEND_DITS: + self.dit_width = 100 # msec (configurable) + self.space_width = 100 # msec (configurable) + self.phrase_gap = 1000 # msec (configurable) + self.num_dits_in_phrase = 5 # number (configurable) + self.num_dits_cur_count = 0 + self.key_was_down = False + self.send_cw_dits = False + self.cw_test_next_ts = None + self.cw_test_next_msec = None + self.cw_phrase_start_ts = None + + self.smeter_text = '' + self.received = '' + self.closing = False + QS.set_sparams(remote_control_head=1, remote_control_slave=0) + + def open(self): + ret = BaseHardware.open(self) + self.remote_ctl_timestamp = time.time() + passw = self.app.local_conf.globals.get("remote_radio_password", "") + passw = passw.strip() + if passw: + del passw + return "Not yet connected to " + self.conf.remote_radio_ip + else: + return "Not yet connected to %s -- Missing Password Here" % self.conf.remote_radio_ip + + def close(self): + print('Closing Remote Control connection') + self.closing = True + t = f'QUIT\n' # Tell Remote Radio we are quitting + self.RemoteCtlSend(t) + self.RemoteCtlClose() + return BaseHardware.close(self) + + def RemoteCtlClose(self): + if self.remote_ctl_socket: + self.remote_ctl_socket.close() + else: + print(' Remote Control TCP socket already closed') + self.remote_ctl_socket = None + self.remote_ctl_connected = False + QS.stop_control_head_remote_sound() + self.app.main_frame.SetConfigText("Disconnected from remote radio " + self.conf.remote_radio_ip) + self.first_heartbeat = True + + def RemoteCtlConnect(self): + if self.remote_ctl_connected: + return True + if not self.remote_ctl_socket: + self.remote_ctl_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.remote_ctl_socket.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, 184) # DSCP "Expedite" (46) + if DEBUG: print("Default timeout for remote_ctl_socket = ", self.remote_ctl_socket.gettimeout()) + self.remote_ctl_socket.settimeout(0.1) # Allow some time for connection response + if DEBUG: print("Our set timeout for remote_ctl_socket = ", self.remote_ctl_socket.gettimeout()) + try: + self.remote_ctl_socket.connect((self.remote_radio_ip, self.remote_ctl_base_port)) + except OSError as err: + if str(err).startswith('[WinError 10056]'): + # Connected, in spite of "error" -- + # This can occur in Windows; Windows networking infrastructure continues attempting to connect, + # even after the connect() call times out (as set in settimeout(), above). + # If Windows connects after the timeout, then, the next time we call connect(), + # it returns WinError 10056, "A connect request was made on an already connected socket". + # Let's accept this "error" as success! + if DEBUG: print('connect() returned WinError 10056; Windows already connected! Good!') + pass + else: + # Not yet connected; errors may be expected/normal, or unexpected. + if str(err).startswith('time'): + # Timeout "error" is normal when we are waiting for Remote Radio server to become available. + if DEBUG: print('connect() returned timeout; still waiting for remote radio server') + elif str(err).startswith('[WinError 10022]'): + # This can occur in Windows; Windows networking infrastructure continues attempting to connect, + # even after the connect() call times out (as set in settimeout(), above). + # If, by the next time we call connect(), it has not yet connected, but is still trying to do so + # as a continuation of the prior call to connect(), it returns WinError 10022, + # "An invalid argument was supplied", which is a little misleading, but okay for us. + # We have not yet connected, but this is an "expected behavior". + if DEBUG: print('connect() returned WinError 10022; invalid argument; still waiting for remote radio server') + elif str(err).startswith('[Errno 103]'): + # This can occur in Linux; still attempting to connect + if DEBUG: print('connect() returned Errno 103; software connection abort; still waiting for remote radio server') + elif str(err).startswith('[Errno 111]'): + # This can occur in Linux; still attempting to connect + if DEBUG: print('connect() returned Errno 111; connection refused; still waiting for remote radio server') + else: + # Unexpected error. Print error info, regardless of DEBUG status. + print("Remote Control socket.connect() error: {0}".format(err)) + return False # Failure to connect + self.remote_ctl_connected = True + self.remote_ctl_socket.settimeout(0.0) # Now that we're connected, don't wait if nothing there + if DEBUG: print("Remote Control connected") + self.app.main_frame.SetConfigText("Connecting to remote radio " + self.conf.remote_radio_ip) + # We have a TCP connection, and the remote will send a challenge token. If we give a valid response + # we will receive "TOKEN_OK" and we can start communication. + return True # Success + + def ChangeFrequency(self, tune, vfo, source='', band='', event=None): + # TODO: Try to get any modifications of freq or vfo from remote Quisk (?) + t = f'FREQ;{tune};{vfo};{source};{band};{self.app.rxFreq};{self.VarDecimGetIndex()}\n' + #print (t) + self.RemoteCtlSend(t) + #BMC if DEBUG: print('Change', source, tune, vfo, band) + return tune, vfo + + def OnSpot(self, level): + pass + + def SendCwDits(self): + ts = time.time() + # Use CW key to start/stop stream of dit phrases + key_down = QS.is_cwkey_down() + if not key_down: + self.key_was_down = False + else: + if not self.key_was_down: # Leading edge detector for key down + self.key_was_down = True + if not self.send_cw_dits: + self.send_cw_dits = True # Start sending dits + self.cw_phrase_start_ts = ts # Beginning of phrase + self.cw_test_next_ts = ts + self.cw_test_next_msec = 0 # Start phrase at 0 msec + self.num_dits_cur_count = 0 + else: + self.send_cw_dits = False # Stop sending dits + if self.send_cw_dits and ts >= self.cw_test_next_ts: + # Send one on/off pair of CW commands (with msec timestamps since start of CW phrase) + t = f'CW;1;{self.cw_test_next_msec}\n' + self.RemoteCtlSend(t) + self.cw_test_next_msec += self.dit_width + t = f'CW;0;{self.cw_test_next_msec}\n' + self.RemoteCtlSend(t) + self.cw_test_next_msec += self.space_width + self.cw_test_next_ts = self.cw_phrase_start_ts + (float(self.cw_test_next_msec) / 1000) + self.num_dits_cur_count += 1 + if self.num_dits_cur_count >= self.num_dits_in_phrase: + # Set up for next phrase + self.cw_test_next_ts = ts + (float(self.phrase_gap) / 1000) + self.cw_phrase_start_ts = self.cw_test_next_ts # Re-start beginning of phrase + self.cw_test_next_msec = 0 # Start phrase at 0 msec + self.num_dits_cur_count = 0 + + def ThreadPrinter(self, *args, **kw): + # Call this to print from (possibly) the sound thread, which must not be slowed down by a print. + # example: print (x, y, end=' ') ----> self.ThreadPrinter(x, y, end=' ') + with self.thread_lock: # Call thread_lock only once; twice will deadlock. + # If print request is from within sound thread, pass the print request to the GUI thread. + if threading.current_thread().name == "QuiskSound": + wx.CallAfter(print, *args, **kw) + else: + print(*args, **kw) + + def PollCwKey(self): # Called by the sound thread + if DEBUG_CW_SEND_DITS: + self.SendCwDits() + return + # Check Quisk key state, send to Remote Radio if change. + # NOTE: Timestamps enable Remote Radio to overcome WiFi/network jitter + ts = time.time() + if not self.cw_phrase_end_ts: + self.cw_phrase_end_ts = ts + self.cw_poll_started_ts = ts + + key_down = QS.is_cwkey_down() + if not self.cw_poll_started: + # Detect Quisk startup with CW key down + if ts - self.cw_poll_started_ts > 0.1: + # Check for key down only within first 1/10 second of running + self.cw_poll_started = True + elif key_down == 1: + # Quisk startup with key down + t = f'Quisk is starting with CW key down! Tx is on, and Rx is blocked until you release CW key.' + self.ThreadPrinter(t) + #dlg = wx.MessageDialog(self.app.main_frame, t, "Quisk start, CW key down", style=wx.OK) + #wx.CallAfter(dlg.ShowModal) + self.cw_phrase_begin_ts = ts + self.cw_poll_started = True + if key_down != self.cw_keydown: + if key_down == 1 and (ts - self.cw_phrase_end_ts) > self.cw_phrase_break_duration_secs: + # First CW key-down since a while ago, re-start timestamp sequence for new CW phrase + self.cw_phrase_begin_ts = ts + cw_event_ts_msecs = int((ts - self.cw_phrase_begin_ts) * 1000) # float secs to int msecs + self.cw_keydown = key_down + t = f'CW;{key_down};{cw_event_ts_msecs}\n' + self.RemoteCtlSend(t) + self.cw_phrase_end_ts = ts # End-of-cw-phrase-detection + if DEBUG_CW_JITTER: self.ThreadPrinter(f'{ts:10.4f} {key_down}, {cw_event_ts_msecs}') + + def HeartBeat(self): # Called at about 10 Hz by the main + if self.closing: # Don't try to connect if we are closing + return + ts = time.time() + if (ts - self.remote_ctl_timestamp) > 1.0 or self.first_heartbeat: + self.remote_ctl_timestamp = ts + if self.remote_ctl_connected: + # Send keep-alive heartbeat command + t = f'HEARTBEAT\n' + self.RemoteCtlSend(t) + else: + # Else continually try to connect + if DEBUG: print('Heartbeat Connect Attempt') + self.RemoteCtlConnect() + self.first_heartbeat = False + self.RemoteCtlRead() + + def RemoteCtlSend(self, text): + # RemoteCtlSend() may be called from sound thread or GUI thread! + # self.thread lock (also used in self.ThreadPrinter) protects against thread collisions. + if not self.remote_ctl_connected: + if DEBUG: self.ThreadPrinter('Cannot send if not TCP connected:', text) + return + if DEBUG: self.ThreadPrinter('Send: ', text, end=' ') + with self.thread_lock: # Do not call ThreadPrinter() from another thread lock! + try: + self.remote_ctl_socket.sendall(text.encode('utf-8', errors='ignore')) + except OSError as err: + errtxt = err + pass + else: + return + self.ThreadPrinter("Closing remote control socket; error in RemoteCtlSend(): {0}".format(errtxt)) + self.RemoteCtlClose() + + def GetSmeter(self): + return self.smeter_text + + def RemoteCtlRead(self): + if not self.remote_ctl_connected: + return + try: # Read any data from the socket + text = self.remote_ctl_socket.recv(1024).decode('utf-8', errors='replace') + except socket.timeout: # This does not work + pass + except socket.error: # Nothing to read + pass + else: # We got some characters + self.received += text + while '\n' in self.received: # A complete response ending with newline is available + reply, self.received = self.received.split('\n', 1) # Split off the reply, save any further characters + reply = reply.strip() # Here is our reply + if DEBUG: print('Rcvd: ', reply) + if reply[0] in 'Qq': + print('Closing Remote Control socket: Q (Quit) from remote radio') + self.RemoteCtlClose() + return + elif reply[0] in 'Mm': + # S-meter text from remote_radio + self.smeter_text = reply[2:] + #print ("Receive smeter", reply[2:]) + elif reply[0:6] == "TOKEN;": + passw = self.app.local_conf.globals.get("remote_radio_password", "") + passw = passw.strip() + if passw: + passw = passw.encode('utf-8') + H = hmac.new(passw, reply[6:].encode('utf-8'), 'sha3_256') + del passw + self.RemoteCtlSend("TOKEN;%s;%d\n" % (H.hexdigest(), self.app.data_width)) + else: + print ("Error: Missing password on control head") + elif reply[0:8] == "TOKEN_OK": + self.app.main_frame.SetConfigText("Connected to remote radio " + self.conf.remote_radio_ip) + QS.start_control_head_remote_sound(self.remote_radio_ip, self.remote_radio_sound_port, self.graph_data_port) + self.CommonInit() # Send initial parameters common to all radios + self.RadioInit() # Send initial parameters peculiar to a given radio + elif reply[0:9] == "TOKEN_BAD": + self.app.main_frame.SetConfigText("Error: Remote radio %s: Security challenge failed" % self.conf.remote_radio_ip) + elif reply[0:13] == "TOKEN_MISSING": + self.app.main_frame.SetConfigText("Error: Remote radio %s has no password" % self.conf.remote_radio_ip) + elif reply[0:9] == "HL2_TEMP;": + setattr(self, "HL2_TEMP", reply[9:]) + elif reply[:3] == 'ERR': + print('Remote Radio returned ' + reply) + else: + print ("Control head received unrecognized command", reply) + + def CommonInit(self): # Send initial frequencies, band, sample rate, etc. to remote + app = self.app + # Frequency and decimation + self.ChangeFrequency(app.txFreq + app.VFO, app.VFO, "NewDecim") + # Band + self.RemoteCtlSend("%s;1\n" % app.lastBand) + # Mode + btn = app.modeButns.GetSelectedButton() + if btn: + self.RemoteCtlSend("%s;%d\n" % (btn.idName, btn.GetIndex())) + # Filter and adjustable bandwidth + name = "Filter 6Slider" + value = app.midiControls[name][0].button.slider_value + self.RemoteCtlSend("%s;%d\n" % (name, value)) + btn = app.filterButns.GetSelectedButton() + if btn: + self.RemoteCtlSend("%s;%d\n" % (btn.idName, btn.GetIndex())) + # AGC and Squelch levels, split offset + self.RemoteCtlSend("Split;0\n") + btn = app.BtnAGC + self.RemoteCtlSend("AGCSQLCH;%d;%d;%d;%d;%d\n" % (btn.slider_value_off, btn.slider_value_on, + app.levelSquelch, app.levelSquelchSSB, app.split_offset)) + idName = "SqlchSlider" + value = app.midiControls[idName][0].button.slider_value + self.RemoteCtlSend("%s;%d\n" % (idName, value)) + # Spot slider + idName = "SpotSlider" + value = app.midiControls[idName][0].button.slider_value + self.RemoteCtlSend("%s;%d\n" % (idName, value)) + # Various buttons + for idName in ("Mute", "NR2", "AGC", "Sqlch", "NB 1", "Notch", "Test 1", "Spot", "FDX", "PTT", "VOX"): + self.RemoteCtlSend("%s;%d\n" % (idName, app.idName2Button[idName].GetIndex())) + # Menus + for menu in (app.NB_menu, app.split_menu, app.freedv_menu, app.smeter_menu): + if menu: + for nid in menu.id2data: + menu_item = menu.FindItemById(nid) + kind = menu_item.GetKind() + if kind == wx.ITEM_RADIO: + if menu_item.IsChecked(): + self.RemoteCtlSend('MENU;%s;%s;1\n' % (menu.menu_name, menu_item.GetItemLabelText())) + elif kind == wx.ITEM_CHECK: + checked = menu_item.IsChecked() + self.RemoteCtlSend('MENU;%s;%s;%d\n' % (menu.menu_name, menu_item.GetItemLabelText(), int(checked))) diff --git a/ac2yd/control_hermes.py b/ac2yd/control_hermes.py new file mode 100644 index 0000000..852558d --- /dev/null +++ b/ac2yd/control_hermes.py @@ -0,0 +1,54 @@ +# This provides access to a remote radio. See ac2yd/remote_common.py and .pdf files for documentation. + +from ac2yd.control_common import ControlCommon + +class Hardware(ControlCommon): + def __init__(self, app, conf): + ControlCommon.__init__(self, app, conf) + self.hermes_code_version = 40 + self.HL2_TEMP = ";;;" + self.var_rates = ['48', '96', '192', '384'] + self.var_index = 0 + #app.bandscope_clock = conf.rx_udp_clock + def ChangeLNA(self, value): + pass + def ChangeAGC(self, value): + pass + def HeartBeat(self): + ControlCommon.HeartBeat(self) + args = self.HL2_TEMP.split(';') + widg = self.app.bottom_widgets + if widg: + widg.text_temperature.SetLabel(args[0]) + widg.text_pa_current.SetLabel(args[1]) + widg.text_fwd_power.SetLabel(args[2]) + widg.text_swr.SetLabel(args[3]) + def RadioInit(self): # Send initial parameters not covered by CommonInit() + idName = "RfLna" + value = self.app.midiControls[idName][0].GetValue() + self.RemoteCtlSend("%s;%d\n" % (idName, value)) + def VarDecimGetChoices(self): # return text labels for the control + return self.var_rates + def VarDecimGetLabel(self): # return a text label for the control + return "Sample rate ksps" + def VarDecimGetIndex(self): # return the current index + return self.var_index + def VarDecimSet(self, index=None): # set decimation, return sample rate + if index is None: # initial call to set rate before the call to open() + rate = self.app.vardecim_set # May be None or from different hardware + else: + rate = int(self.var_rates[index]) * 1000 + if rate == 48000: + self.var_index = 0 + elif rate == 96000: + self.var_index = 1 + elif rate == 192000: + self.var_index = 2 + elif rate == 384000: + self.var_index = 3 + else: + self.var_index = 0 + rate = 48000 + return rate + def VarDecimRange(self): + return (48000, 384000) diff --git a/ac2yd/control_hiqsdr.py b/ac2yd/control_hiqsdr.py new file mode 100644 index 0000000..370e739 --- /dev/null +++ b/ac2yd/control_hiqsdr.py @@ -0,0 +1,40 @@ +# This provides access to a remote radio. See ac3yd/remote_common.py and .pdf files for documentation. + +from ac2yd.control_common import ControlCommon + +class Hardware(ControlCommon): + def __init__(self, app, conf): + ControlCommon.__init__(self, app, conf) + self.index = 0 + self.rx_udp_clock = 122880000 + self.decimations = [] # supported decimation rates + for dec in (40, 20, 10, 8, 5, 4, 2): + self.decimations.append(dec * 64) + self.decimations.append(80) + self.decimations.append(64) + def RadioInit(self): # Send initial parameters not covered by CommonInit() + pass + def VarDecimGetChoices(self): # return text labels for the control + clock = self.rx_udp_clock + l = [] # a list of sample rates + for dec in self.decimations: + l.append(str(int(float(clock) / dec / 1e3 + 0.5))) + return l + def VarDecimGetLabel(self): # return a text label for the control + return "Sample rate ksps" + def VarDecimGetIndex(self): # return the current index + return self.index + def VarDecimSet(self, index=None): # set decimation, return sample rate + if index is None: # initial call to set decimation before the call to open() + rate = self.application.vardecim_set # May be None or from different hardware + try: + dec = int(float(self.rx_udp_clock // rate + 0.5)) + self.index = self.decimations.index(dec) + except: + self.index = 0 + else: + self.index = index + dec = self.decimations[self.index] + return int(float(self.rx_udp_clock) / dec + 0.5) + def VarDecimRange(self): + return (48000, 960000) diff --git a/ac2yd/control_softrock.py b/ac2yd/control_softrock.py new file mode 100644 index 0000000..f9754cf --- /dev/null +++ b/ac2yd/control_softrock.py @@ -0,0 +1,9 @@ +# This provides access to a remote radio. See ac2yd/remote_common.py and .pdf files for documentation. + +from ac2yd.control_common import ControlCommon + +class Hardware(ControlCommon): + def __init__(self, app, conf): + ControlCommon.__init__(self, app, conf) + def RadioInit(self): # Send initial parameters not covered by CommonInit() + pass diff --git a/ac2yd/remote.c b/ac2yd/remote.c new file mode 100644 index 0000000..6f53e4c --- /dev/null +++ b/ac2yd/remote.c @@ -0,0 +1,748 @@ +/* + This software is Copyright (C) 2021-2022 by Ben Cahill and 2006-2022 by James C. Ahlstrom, + and is licensed for use under the GNU General Public License (GPL). + See http://www.opensource.org. + Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!! +*/ + +#define PY_SSIZE_T_CLEAN +#include +#include +#include +#include +#include +#include + +#ifdef MS_WINDOWS +#include +#include +#else + +#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#include +#if defined(BSD) +#include +#endif +#endif + +#include +#include +#include +#include +#endif + +#include "../quisk.h" +#include "../filter.h" + +#define GRAPH_DATA_SCALE 163 +#define MAX_UDP_INT16_T 600 +#define UDP_SEND_INT16 200 + +#define REMOTE_DEBUG 0 //BMC TODO: Make this a configuration option + +static SOCKET remote_radio_sound_socket = INVALID_SOCKET; // send radio sound to control_head, receive mic samples +static SOCKET control_head_sound_socket = INVALID_SOCKET; // receive radio sound from remote_radio, send mic samples +static SOCKET remote_radio_graph_socket = INVALID_SOCKET; // send graph data to control_head +static SOCKET control_head_graph_socket = INVALID_SOCKET; // receive graph data from remote_radio +static int control_head_sound_socket_started = 0; // sound stream started on the control head +static int remote_radio_sound_socket_started = 0; // sound stream started on the remote radio +static int control_head_graph_socket_started = 0; // graph data stream started on the control head +static int remote_radio_graph_socket_started = 0; // graph data stream started on the remote radio +static int control_head_data_width; // app.data_width of control head for graph data +static int packets_sent; +static int packets_recd; + +// Receive stereo 16-bit pcm radio speaker sound on the control head via UDP +int read_remote_radio_sound_socket(complex double * cSamples) +{ + int i, bytes, nInt16, nSamples; + int16_t buf[UDP_SEND_INT16]; + double samp_r, samp_l; + struct timeval tm_wait; + fd_set fds; + static struct quisk_cHB45Filter HalfBand; + static struct quisk_cFilter cFiltInterp3; + static int init_filters=1; + + if (control_head_sound_socket == INVALID_SOCKET) + return 0; + if (init_filters) { + init_filters = 0; + memset(&HalfBand, 0, sizeof(struct quisk_cHB45Filter)); + quisk_filt_cInit(&cFiltInterp3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double)); + } + // Signal far end (server) that we're ready (this sends our address/port to far end) + if (!control_head_sound_socket_started) { + QuiskPrintf("read_remote_radio_sound_socket() sending 'rr'\n"); + bytes = send(control_head_sound_socket, "rr\n", 3, 0); + if (bytes != 3) + QuiskPrintf("read_remote_radio_sound_socket(), sendto(): %s\n", strerror(errno)); + } + + // read all available packets, one per loop + nSamples = 0; + while (1) { + tm_wait.tv_sec = 0; + tm_wait.tv_usec = 0; + FD_ZERO (&fds); + FD_SET (control_head_sound_socket, &fds); + if (select(control_head_sound_socket + 1, &fds, NULL, NULL, &tm_wait) != 1) { + //BMC QuiskPrintf("read_remote_radio_sound_socket(): select returned %i\n", retval); + break; + } + bytes = recv(control_head_sound_socket, (char *)buf, UDP_SEND_INT16 * 2, 0); + if (bytes < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) + QuiskPrintf("read_remote_radio_sound_socket(), recv(): %s\n", strerror(errno)); + break; + } + if (bytes > 0) { + control_head_sound_socket_started = 1; + nInt16 = bytes / 2; + for (i = 0; i < nInt16; i += 2) { + samp_r = buf[i]; + samp_l = buf[i + 1]; + cSamples[nSamples++] = (samp_r + I * samp_l) / CLIP16 * CLIP32; + } + } + } // while(1) + + nSamples = quisk_cInterpolate(cSamples, nSamples, &cFiltInterp3, 3); + nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand); + + return nSamples; +} + +// Receive stereo 16-bit pcm microphone samples at the remote radio via UDP +int read_remote_mic_sound_socket(complex double * cSamples) +{ + int i, bytes, nInt16, nSamples; + int16_t buf[UDP_SEND_INT16]; + double samp_r, samp_l; + struct timeval tm_wait; + fd_set fds; + static struct quisk_cHB45Filter HalfBand; + static struct quisk_cFilter cFiltInterp3; + static int init_filters=1; + + if (remote_radio_sound_socket == INVALID_SOCKET) + return 0; + if (init_filters) { + init_filters = 0; + memset(&HalfBand, 0, sizeof(struct quisk_cHB45Filter)); + quisk_filt_cInit(&cFiltInterp3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double)); + } + + // read all available packets, one per loop + nSamples = 0; + while (1) { + tm_wait.tv_sec = 0; + tm_wait.tv_usec = 0; + FD_ZERO (&fds); + FD_SET (remote_radio_sound_socket, &fds); + if (select(remote_radio_sound_socket + 1, &fds, NULL, NULL, &tm_wait) != 1) { + //BMC QuiskPrintf("read_remote_mic_sound_socket(): select returned %i\n", retval); + break; + } + bytes = recv(remote_radio_sound_socket, (char *)buf, UDP_SEND_INT16 * 2, 0); + if (bytes < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) + QuiskPrintf("read_remote_mic_sound_socket(), recv(): %s\n", strerror(errno)); + break; + } + if (bytes > 0) { + nInt16 = bytes / 2; + for (i = 0; i < nInt16; i += 2) { + samp_r = buf[i]; + samp_l = buf[i + 1]; + cSamples[nSamples++] = (samp_r + I * samp_l) / CLIP16 * CLIP32; + } + } + } // while(1) + + nSamples = quisk_cInterpolate(cSamples, nSamples, &cFiltInterp3, 3); + nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand); + + return nSamples; +} +// Send stereo 16-bit pcm sound via UDP +// This code acts as UDP server for radio sound (on remote radio) or mic sound (on control head) +#define MAX_SAMPLES_FOR_REMOTE_SOUND 15000 +#define RX_BUFFER_SIZE 64 + +// Send microphone samples from the control head to the remote radio +void send_remote_mic_sound_socket(complex double * cSamples, int nSamples) +{ + int i; + ssize_t sent; + static int buffer_index=0; + static int16_t buffer[UDP_SEND_INT16]; + static struct quisk_cHB45Filter HalfBand; + static struct quisk_cFilter cFiltDecim3; + static int init_filters=1, size_cBuf=0; + static complex double * cBuf=NULL; + + if (init_filters) { + init_filters = 0; + memset(&HalfBand, 0, sizeof(struct quisk_cHB45Filter)); + quisk_filt_cInit(&cFiltDecim3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double)); + } + if (nSamples > size_cBuf) { + size_cBuf = nSamples; + cBuf = (complex double *)realloc(cBuf, size_cBuf * sizeof(complex double)); + } + if (control_head_sound_socket == INVALID_SOCKET) + return; + + if (!control_head_sound_socket_started) + return; + memcpy(cBuf, cSamples, nSamples * sizeof(complex double)); // Do not alter cSamples + // Reduce sample rate from 48 to 8 ksps + nSamples = quisk_cDecim2HB45(cBuf, nSamples, &HalfBand); + nSamples = quisk_cDecimate(cBuf, nSamples, &cFiltDecim3, 3); + // Convert format from complex double to stereo pairs of 16-bit PCM samples. + // Buffer samples until UDP_SEND_INT16 are available, and then send the block. + for (i = 0; i < nSamples; i++) { + buffer[buffer_index++] = (int16_t)(creal(cBuf[i]) * (double)CLIP16 / CLIP32); + buffer[buffer_index++] = (int16_t)(cimag(cBuf[i]) * (double)CLIP16 / CLIP32); + if (buffer_index >= UDP_SEND_INT16) { + sent = send(control_head_sound_socket, (const char *)buffer, buffer_index * 2, 0); + if (sent != buffer_index * 2) + QuiskPrintf("send_remote_mic_sound_socket(), send(): %s\n", strerror(errno)); + buffer_index = 0; + } + } +} + +// Send radio speaker sound from the remote radio to the control head +void send_remote_radio_sound_socket(complex double * cSamples, int nSamples) +{ // Send nSamples samples. Each sample is sent as two shorts (4 bytes) of L/R audio data. + int i, N, sent; + static int16_t sound_lr[UDP_SEND_INT16]; + static int udp_size = 0; // Keep track of UDP payload size, in shorts + char buf[RX_BUFFER_SIZE]; // For startup message + int recv_len; + int retval; + SOCKET * sock = &remote_radio_sound_socket; + static struct quisk_cHB45Filter HalfBand; + static struct quisk_cFilter cFiltDecim3; + static int init_filters=1, size_cBuf=0; + static complex double * cBuf=NULL; + +#if REMOTE_DEBUG > 0 //BMC debug + // measure/monitor tools: + static float callcount = 0; + static float sampcount = 0; + static uint64_t bunchcount = 0; + static float callcount_total = 0; + static float sampcount_total = 0; + static double prior_ts = 0; + static double delta_total = 0; +#if REMOTE_DEBUG > 1 //BMC debug + static double prior_packet_ts = 0.0; + double now; +#endif +#endif + + if (*sock == INVALID_SOCKET) + return; + + if (init_filters) { + init_filters = 0; + memset(&HalfBand, 0, sizeof(struct quisk_cHB45Filter)); + quisk_filt_cInit(&cFiltDecim3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double)); + } + if (nSamples > size_cBuf) { + size_cBuf = nSamples; + cBuf = (complex double *)realloc(cBuf, size_cBuf * sizeof(complex double)); + } + +#if REMOTE_DEBUG > 0 //BMC debug + callcount++; +#endif + // Wait for far end (client) to send its opening greetings, so we can grab its network address/port + if (!remote_radio_sound_socket_started) { + struct sockaddr_in far_addr; +#ifdef MS_WINDOWS + int addr_len = sizeof(struct sockaddr_in); +#else + socklen_t addr_len = sizeof(struct sockaddr_in); +#endif + struct timeval tm_wait; + fd_set fds; + tm_wait.tv_sec = 0; + tm_wait.tv_usec = 0; + FD_ZERO (&fds); + FD_SET (*sock, &fds); + if ((retval = select(*sock + 1, &fds, NULL, NULL, &tm_wait)) != 1) { + //BMC QuiskPrintf("send_remote_sound_socket(): select returned %i\n", retval); + return; + } + // Receive short msg, grab far end address + if ((recv_len = recvfrom(*sock, buf, RX_BUFFER_SIZE, 0, (struct sockaddr *) &far_addr, &addr_len)) == -1) { + QuiskPrintf("send_remote_sound_socket(), recvfrom(): %s\n", strerror(errno)); + return; + } + else if(recv_len > 0) { + if (recv_len >= RX_BUFFER_SIZE) + buf[RX_BUFFER_SIZE - 1] = '\n'; + else + buf[recv_len] = '\n'; + QuiskPrintf("send_remote_sound_socket(): recv_len = %i, %s", recv_len, buf); + if (connect(*sock, (const struct sockaddr *)&far_addr, sizeof(far_addr)) != 0) { + QuiskPrintf("send_remote_sound_socket), connect(): %s\n", strerror(errno)); + close(*sock); + *sock = INVALID_SOCKET; + } + else + remote_radio_sound_socket_started = 1; + } + } + + memcpy(cBuf, cSamples, nSamples * sizeof(complex double)); // Do not alter cSamples + // Reduce sample rate from 48 to 8 ksps + nSamples = quisk_cDecim2HB45(cBuf, nSamples, &HalfBand); + nSamples = quisk_cDecimate(cBuf, nSamples, &cFiltDecim3, 3); + // Convert format from complex double to stereo pairs of 16-bit PCM samples, send to client + for (i = 0; i < nSamples; i++) { + sound_lr[udp_size++] = (int16_t)(creal(cBuf[i]) * (double)CLIP16 / CLIP32); + sound_lr[udp_size++] = (int16_t)(cimag(cBuf[i]) * (double)CLIP16 / CLIP32); + if (udp_size >= UDP_SEND_INT16) { + N = UDP_SEND_INT16; + udp_size = 0; + sent = send(*sock, (char *)sound_lr, N * 2, 0); + if (sent != N * 2) + QuiskPrintf("send_remote_sound_socket(), send(): %s\n", strerror(errno)); +#if REMOTE_DEBUG > 0 //BMC debug + else { + sampcount += sent/4; + packets_sent++; +#if REMOTE_DEBUG > 1 //BMC debug + now = QuiskTimeSec(); + QuiskPrintf("%f, send_remote_sound_socket(): now - prior = %f, samples = %u, sampcount = %lu\n", + now, now - prior_packet_ts, sent / 4, sampcount); + prior_packet_ts = now; +#endif + } +#endif + } +#if REMOTE_DEBUG > 1 //BMC debug + else { + now = QuiskTimeSec(); + QuiskPrintf("%f, send_remote_sound_socket(): nSamples = %i\n", now, nSamples); + } +#endif + } +#if REMOTE_DEBUG > 0 //BMC debug + if (callcount >= 200) { + double new_ts = QuiskTimeSec(); + double delta = new_ts - prior_ts; + prior_ts = new_ts; +#if REMOTE_DEBUG > 1 //BMC every 200 + QuiskPrintf("send_remote_sound_socket CURRENT calls: %lu, samples %lu, deltasec %f\n", callcount, sampcount, delta); + QuiskPrintf("%f: send_remote_sound_socket CURRENT RATES (HZ): calls %f, samples %f\n", new_ts, callcount / delta, sampcount / delta); +#endif + if (bunchcount > 0) { // skip the initial bunch; prebuf may distort some numbers(?) + callcount_total += callcount; + sampcount_total += sampcount; + delta_total += delta; + } + if (bunchcount % 10 == 0 && bunchcount > 0) { + QuiskPrintf("%f: send_remote_sound_socket SUMMARY calls: %f, samples %f, deltasec %f\n", + new_ts, callcount_total, sampcount_total, delta_total); + QuiskPrintf("%f: send_remote_sound_socket SUMMARY RATES (HZ): calls %f, samples %f\n", + new_ts, callcount_total / delta_total, sampcount_total / delta_total); + } + bunchcount++; + callcount = 0; + sampcount = 0; + } +#endif +} + +// Send graph data via UDP from the remote radio to the control head +void send_graph_data(double * fft_avg, int fft_size, double zoom, double deltaf, int fft_sample_rate, double scale) +{ + static double * pixels = NULL; + static int n_pixels = 0; + static uint8_t sequence = 0; + uint8_t flags; + int16_t buffer[MAX_UDP_INT16_T]; + int16_t block; + int pixel_index, buffer_index; + double d1, d2; + ssize_t sent; + char buf[RX_BUFFER_SIZE]; // For startup message + int recv_len; + struct sockaddr_in far_addr; +#ifdef MS_WINDOWS + int addr_len = sizeof(struct sockaddr_in); +#else + socklen_t addr_len = sizeof(struct sockaddr_in); +#endif + + if (remote_radio_graph_socket == INVALID_SOCKET) + return; + if ( !control_head_data_width) + return; + if ( !remote_radio_graph_socket_started) { + // Receive short msg, grab far end address + // Receive from control head is necessary to establish a path through NAT + if ((recv_len = recvfrom(remote_radio_graph_socket, buf, RX_BUFFER_SIZE, 0, (struct sockaddr *) &far_addr, &addr_len)) < 2) { + return; + } + else { + if (connect(remote_radio_graph_socket, (const struct sockaddr *)&far_addr, sizeof(far_addr)) != 0) { + QuiskPrintf("send_remote_graph_socket), connect(): %s\n", strerror(errno)); + close(remote_radio_graph_socket); + remote_radio_graph_socket = INVALID_SOCKET; + return; + } + else { + remote_radio_graph_socket_started = 1; + } + } + } + if (control_head_data_width > n_pixels) { + n_pixels = control_head_data_width; + if (pixels) + free(pixels); + pixels = (double *)malloc(n_pixels * sizeof(double)); + } + if ( ! fft_avg) { // send dummy graph data + send(remote_radio_graph_socket, "dum", 3, 0); + return; + } + copy2pixels(pixels, control_head_data_width, fft_avg, fft_size, zoom, deltaf, fft_sample_rate); + // Send multiple 16-bit data blocks: {flags, sequence}, block number, 16-bit graph data + // 8-bit flags: + // bit 0: clip indicator + // 8-bit sequence: 0, 1, 2, ..., 255 + block = 0; + pixel_index = 0; + while (pixel_index < control_head_data_width) { + if (quisk_get_overrange()) + flags = 0x01; + else + flags = 0x00; + buffer[0] = flags << 8 | sequence; + buffer[1] = block; + buffer_index = 2; + while (buffer_index < MAX_UDP_INT16_T && pixel_index < control_head_data_width) { + d1 = pixels[pixel_index++]; + if (fabs(d1) < 1e-40) // avoid log10(0) + d1 = 1E-40; + d2 = 20.0 * log10(d1) - scale; + if (d2 < -200) + d2 = -200; + else if (d2 > 0) + d2 = 0; + buffer[buffer_index++] = (int16_t)lround(d2 * GRAPH_DATA_SCALE); + } + sent = send(remote_radio_graph_socket, (const char *)buffer, buffer_index * 2, 0); + if (sent != buffer_index * 2) + QuiskPrintf("send_graph_data(), send(): %s\n", strerror(errno)); + block++; + } + sequence += 1; +} + +// Receive graph data via UDP on the control head +int receive_graph_data(double * fft_avg) +{ + int i, i1, i2; + ssize_t count; + uint8_t seq, flags; + int16_t buffer[MAX_UDP_INT16_T]; + int16_t block; + static int16_t * pixels = NULL; + static int n_pixels = 0; + static int total = 0; + static int16_t sequence = 0; + + if (control_head_graph_socket == INVALID_SOCKET) + return 0; + // Signal far end (server) that we're ready (this sends our address/port to far end) + if ( !control_head_graph_socket_started) { + i = send(control_head_graph_socket, "rr\n", 3, 0); + if (i != 3) + QuiskPrintf("receive_graph_data(), send(): %s\n", strerror(errno)); + } + if (n_pixels < data_width) { + n_pixels = data_width; + if (pixels) + free(pixels); + pixels = (int16_t *)malloc(n_pixels * sizeof(int16_t)); + } + count = recv(control_head_graph_socket, (char *)buffer, MAX_UDP_INT16_T * 2, 0); + count /= 2; // convert to int16_t + if (count > 2) { + control_head_graph_socket_started = 1; + flags = buffer[0] >> 8; + if (flags & 0x01) // Clip + quisk_sound_state.overrange++; + seq = buffer[0] & 0xFF; + if (seq != sequence) { // new graph data + sequence = seq; + total = 0; + } + block = buffer[1]; + count -= 2; // number of 16-bit graph data items + i1 = block * (MAX_UDP_INT16_T - 2); + i2 = i1 + count; + if (i1 >= 0 && i2 <= data_width) { + memcpy(pixels + i1, buffer + 2, count * 2); + total += count; + if (total == data_width) { + for (i = 0; i < data_width; i++) + fft_avg[i] = (double)pixels[i] / GRAPH_DATA_SCALE; + return data_width; + } + } + } + return 0; +} + + +static int start_winsock() +{ +#ifdef MS_WINDOWS + WORD wVersionRequested = MAKEWORD(2, 2); + WSADATA wsaData; + + if (WSAStartup(wVersionRequested, &wsaData) != 0) { + QuiskPrintf("start_winsock(): %s\n", strerror(errno)); + return 0; // failure to start winsock + } +#endif + return 1; +} + + + + + +static void open_and_bind_socket(SOCKET * sock, char * ip, int port, int sndsize, char * name, int non_block) +{ + struct sockaddr_in bind_addr; + const char enable = 1; // for sockopt +#ifndef MS_WINDOWS + int tos = 184; // DSCP "Expedite" (46) +#endif + + if (!start_winsock()) { + QuiskPrintf("open_and_bind_socket for %s: Failure to start WinSock\n", name); + return; + } + + *sock = socket(PF_INET, SOCK_DGRAM, 0); + if (*sock != INVALID_SOCKET) { + setsockopt(*sock, SOL_SOCKET, SO_SNDBUF, (char *)&sndsize, sizeof(sndsize)); + setsockopt(*sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)); +#ifndef MS_WINDOWS + setsockopt(*sock, IPPROTO_IP, IP_TOS, &tos, sizeof(tos)); +#endif + + // bind to this computer for receiving (and reading far-end address from client) + memset((char *) &bind_addr, 0, sizeof(bind_addr)); + bind_addr.sin_family = AF_INET; + bind_addr.sin_port = htons(port); + bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(*sock, (const struct sockaddr *)&bind_addr, sizeof(bind_addr)) != 0) { + QuiskPrintf("open_and_bind_socket(), bind(): %s\n", strerror(errno)); + close(*sock); + *sock = INVALID_SOCKET; + } + else if (non_block) { +#ifdef MS_WINDOWS + unsigned long one = 1; + ioctlsocket(*sock, FIONBIO, &one); // set non-blocking +#else + int flags; + flags = fcntl(*sock, F_GETFL, 0); // set non-blocking + fcntl(*sock, F_SETFL, flags | O_NONBLOCK); +#endif + } + } + if (*sock == INVALID_SOCKET) { + QuiskPrintf("open server %s: Failure to open socket\n", name); + } + else { + QuiskPrintf("open server %s: opened socket %s port %i\n", name, ip, port); + } +} + + +static void open_and_connect_socket(SOCKET * sock, char * ip, int port, int sndsize, char * name, int non_block) +{ + struct sockaddr_in Addr; + const char enable = 1; // for sockopt + + if (!start_winsock()) { + QuiskPrintf("open_and_connect_socket for %s: Failure to start WinSock\n", name); + return; + } + + *sock = socket(PF_INET, SOCK_DGRAM, 0); + if (*sock != INVALID_SOCKET) { + setsockopt(*sock, SOL_SOCKET, SO_RCVBUF, (char *)&sndsize, sizeof(sndsize)); + setsockopt(*sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)); + + // set far-end address structure to enable sending initial packet to get things started + Addr.sin_family = AF_INET; + Addr.sin_port = htons(port); +#ifdef MS_WINDOWS + Addr.sin_addr.S_un.S_addr = inet_addr(ip); +#else + inet_aton(ip, &Addr.sin_addr); +#endif + if (connect(*sock, (const struct sockaddr *)&Addr, sizeof(Addr)) != 0) { + close(*sock); + *sock = INVALID_SOCKET; + } + else if (non_block) { +#ifdef MS_WINDOWS + unsigned long one = 1; + ioctlsocket(*sock, FIONBIO, &one); // set non-blocking +#else + int flags; + flags = fcntl(*sock, F_GETFL, 0); // set non-blocking + fcntl(*sock, F_SETFL, flags | O_NONBLOCK); +#endif + } + } + if (*sock == INVALID_SOCKET) { + QuiskPrintf("open client %s: Failure to open socket\n", name); + } + else { + QuiskPrintf("open client %s: opened socket %s port %i\n", name, ip, port); + } +} + + +static void close_socket(SOCKET * sock, char * name) +{ + if (*sock != INVALID_SOCKET) { + close(*sock); + *sock = INVALID_SOCKET; +#ifdef MS_WINDOWS + WSACleanup(); +#endif + QuiskPrintf("%s: closed socket\n", name); + } + else { + QuiskPrintf("%s: socket already closed\n", name); + } +} + +// start running UDP remote sound on control_head ... +// ... receive radio sound from remote_radio, send mic sound to remote_radio +PyObject * quisk_start_control_head_remote_sound(PyObject * self, PyObject * args) +{ + int radio_sound_port; + int graph_data_port; + int sndsize = 48000; + char * remote_radio_ip; // IP address of far end + char * name; + SOCKET * sock; + + if (!PyArg_ParseTuple (args, "sii", &remote_radio_ip, &radio_sound_port, &graph_data_port)) + return NULL; + + name = "radio sound from remote_radio"; + sock = &control_head_sound_socket; + open_and_connect_socket(sock, remote_radio_ip, radio_sound_port, sndsize, name, 0); + + name = "graph data from remote_radio"; + sock = &control_head_graph_socket; + open_and_connect_socket(sock, remote_radio_ip, graph_data_port, 1024 * 8, name, 1); + + packets_sent = 0; + packets_recd = 0; + + return Py_None; +} + +// stop running UDP remote sound on control_head +PyObject * quisk_stop_control_head_remote_sound(PyObject * self, PyObject * args) +{ + char * name; + SOCKET * sock; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + + name = "radio sound from remote_radio"; + sock = &control_head_sound_socket; + close_socket(sock, name); + + name = "graph data from remote_radio"; + sock = &control_head_graph_socket; + close_socket(sock, name); + + control_head_sound_socket_started = 0; // reset for next time + remote_radio_sound_socket_started = 0; + control_head_graph_socket_started = 0; + remote_radio_graph_socket_started = 0; + + QuiskPrintf("total packets sent = %i, recd = %i\n", packets_sent, packets_recd); + + return Py_None; +} + +// start running UDP remote sound on remote_radio ... +// ... send radio sound to control_head, receive mic sound from control_head +PyObject * quisk_start_remote_radio_remote_sound(PyObject * self, PyObject * args) +{ + int radio_sound_port; + int graph_data_port; + int sndsize = 48000; + char * control_head_ip; // IP address of far end + char * name; + SOCKET * sock; + + if (!PyArg_ParseTuple (args, "siii", &control_head_ip, &radio_sound_port, + &graph_data_port, &control_head_data_width)) + return NULL; + + name = "radio sound to control_head"; + sock = &remote_radio_sound_socket; + open_and_bind_socket(sock, control_head_ip, radio_sound_port, sndsize, name, 0); + + name = "graph data to control_head"; + sock = &remote_radio_graph_socket; + open_and_bind_socket(sock, control_head_ip, graph_data_port, 1024 * 8, name, 1); + + packets_sent = 0; + packets_recd = 0; + + return Py_None; +} + +// stop running UDP remote sound on remote_radio +PyObject * quisk_stop_remote_radio_remote_sound(PyObject * self, PyObject * args) +{ + char * name; + SOCKET * sock; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + + name = "radio sound to control_head"; + sock = &remote_radio_sound_socket; + close_socket(sock, name); + + name = "graph data to control_head"; + sock = &remote_radio_graph_socket; + close_socket(sock, name); + + control_head_sound_socket_started = 0; // reset for next time + remote_radio_sound_socket_started = 0; + control_head_graph_socket_started = 0; + remote_radio_graph_socket_started = 0; + control_head_data_width = 0; + + QuiskPrintf("total packets sent = %i, recd = %i\n", packets_sent, packets_recd); + + return Py_None; +} diff --git a/ac2yd/remote_common.py b/ac2yd/remote_common.py new file mode 100644 index 0000000..43d996a --- /dev/null +++ b/ac2yd/remote_common.py @@ -0,0 +1,439 @@ +# THIS FILE IS FOR USE ON A REMOTELY CONTROLLED "REMOTE_RADIO" COMPUTER +# RUNNING QUISK TO CONTROL ATTACHED ACTUAL/REAL RADIO HARDWARE. +# IT CONNECTS BY NETWORK TO A SEPARATE "CONTROL_HEAD" COMPUTER ALSO RUNNING QUISK. +# +# This software is Copyright (C) 2021-2022 by Ben Cahill and 2006-2022 by James C. Ahlstrom., +# and is licensed for use under the GNU General Public License (GPL). +# See http://www.opensource.org. +# Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!! +# +# This file, remote_common.py, along with a radio-specific file, e.g. +# remote_softrock.py, remote_hermes.py, or similar, allows a radio-less (control_head) Quisk, +# running on a separate computer, to connect to this (remote_radio) instance of Quisk, +# which has an actual, real radio attached. +# +# The control_head Quisk must use control_common.py, along with a radio-specific file, +# e.g. control_softrock.py, control_hermes.py, or similar, to communicate with this +# remote_radio Quisk via a network connection. +# +# This remote_radio computer should be set up with a static IP address, so that you know +# where to point the control_head. The control head computer may, however, use dynamic +# addressing; this remote_radio computer will read the control head address when the +# remote control connection is made. +# +# The main control interface between control_head and remote_radio is via a TCP port; +# this uses very low bandwidth. All functional control, including CW keying, is done via this port. +# +# There are 2 additional ports, both UDP, using low to moderate bandwidth: +# -- Send graph/waterfall data to the control_head +# -- Send radio sound to the control_head, and receive mic sound +# These use sequential port numbers based on the TCP port number self.remote_ctl_base_port. +# If you need to change the default base port number, you can edit the line in this file +# that looks like (without the #): +# +# self.remote_ctl_base_port = 4585 # Remote Control base TCP port +# self.graph_data_port = self.remote_ctl_base_port + 1 # UDP port for graph data from the remote +# self.remote_radio_sound_port = self.remote_ctl_base_port + 2 # UDP port for radio sound and mic samples +# +# +# Make sure to edit the corresponding line in control_common.py to match ports!! +# +# This remote_radio Quisk/computer is assumed to track the connected control_head Quisk/computer; +# no attempt is made by the control_head to verify the remote_radio Quisk's tuning frequency, mode, etc. +# Snap-to Rx tuning for CW works on the control_head Quisk by virtue of graph/waterfall data +# received from the remote_radio Quisk. +# +# To see detailed log output of CW key timing, set DEBUG_CW_JITTER = 1. +# To additionally see, when CW commands are pending, timestamps of when PollCwKey is called +# (e.g. to check thread scheduling behavior), set DEBUG_CW_JITTER = 2. +# To send "perfect" bursts of CW dits from the control_head, set DEBUG_CW_SEND_DITS = 1 +# in the control_head's quisk_hardware_control_head.py. + +DEBUG_CW_JITTER = 0 +DEBUG = 0 + +from collections import deque # for CW event queue +import socket, time, traceback, string, hmac, secrets, json +import _quisk as QS +from quisk_widgets import * + +class Remot: # Remote comtrol base class + def __init__(self, app, conf): + self.app = app # Access Quisk class App (Python) functions + self.conf = conf + self.token = "abc" + self.token_time = 0 + + self.control_head_ip = None # IP of control_head compter (read upon connection) + self.remote_ctl_base_port = 4585 # Base of ports for remote connection (maybe edit this) + self.remote_ctl_socket = None + self.remote_ctl_connection = None + self.remote_ctl_heartbeat_ts = None + self.remote_ctl_heartbeat_timeout = 10.0 # Close our connection if we don't hear heartbeat from Control Head + self.graph_data_port = self.remote_ctl_base_port + 1 + self.remote_radio_sound_port = self.remote_ctl_base_port + 2 + + self.cw_delay_secs = 0.020 # time delay to absorb WiFi jitter, in secs + self.cw_phrase_begin_ts = None # timestamp of beginning of cw phrase + self.cw_next_event_ts = None + self.cw_next_keydown = None + self.cw_event_queue = deque() + self.cw_key_down = 0 # Tx-enable management + self.cw_tx_enable = 0 + + self.received = '' + self.cmd_text = None # cmd received from client (remote head) + self.cmd = None # cmd received from client (remote head) + self.params = None # params = the string following the command + self.extended = None + self.split_mode = 0 + + print('Remote Overlay Initialized!') + + def open(self): + self.token = "abc" + self.remote_ctl_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.remote_ctl_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.remote_ctl_socket.bind(('', self.remote_ctl_base_port)) # '' == INADDR_ANY + self.remote_ctl_socket.settimeout(0.0) + self.remote_ctl_socket.listen(1) # listen for TCP connection from just one client + print('Remote Overlay Opened!') + # Return an informative message for the config screen. + # This method must return a string showing whether the open succeeded or failed. + # Here, we are over-writing the string set by quisk_hardware_model.py + t = f'Quisk Remote Controlled Radio {self.app.width}x{self.app.height} {self.app.graph_width} {self.app.data_width}' + return t + #BMC return ret + + def close(self): # Close the listening socket, then the connection socket + if self.remote_ctl_socket: + self.remote_ctl_socket.close() + self.remote_ctl_socket = None + self.token = "abc" + if self.remote_ctl_connection: + print('Closing Remote Control connection: close') + self.RemoteCtlClose(True) + + def RemoteCtlOpen(self): + try: + self.remote_ctl_connection, address = self.remote_ctl_socket.accept() + except: + return + else: + self.app.remote_control_slave = True + QS.set_sparams(remote_control_slave=1) + self.token = secrets.token_hex(32) + self.remote_ctl_connection.settimeout(0.0) + self.remote_ctl_heartbeat_ts = time.time() + if DEBUG: print('Remote Control connection: ', self.remote_ctl_connection, ' address: ', address) + self.control_head_ip = address[0] + print ("Remote control connection from", self.control_head_ip) + self.RemoteCtlSend("TOKEN;" + self.token + "\n") + self.token_time = time.time() + + def RemoteCtlClose(self, send_quit): + self.StopTransmit() + if self.remote_ctl_connection: + if send_quit: + self.RemoteCtlSend('Q\n') + if self.remote_ctl_connection: + self.remote_ctl_connection.close() + self.remote_ctl_connection = None + QS.stop_remote_radio_remote_sound() + self.app.remote_control_slave = False + QS.set_sparams(remote_control_slave=0) + + def RemoteCtlSend(self, text): + # Send text back to the client + if not self.remote_ctl_connection: + return + if isinstance(text, str): + text = text.encode('utf-8', errors='ignore') + try: + self.remote_ctl_connection.sendall(text) + except socket.error: + print('Closing Remote Control connection: sendall() failed. Sent text:\n ' + text.decode('utf-8')) + # NOTE: Cannot send 'Q' to Control Head here; sendall() isn't working! + self.RemoteCtlClose(False) + + def ErrParam(self): # Invalid parameter + t = 'ERR_PARAM: ' + self.cmd_text + '\n' + print(t) + self.RemoteCtlSend(t) + def ErrUnsupported(self): # Command recognized but not supported (because of either H/W or configuration) + t = 'ERR_UNSUPPORTED: ' + self.cmd_text + '\n' + print(t) + self.RemoteCtlSend(t) + def ErrUnrecognized(self): # Unrecognized command + t = 'ERR_UNRECOGNIZED_CMD: ' + self.cmd_text + '\n' + print(t) + self.RemoteCtlSend(t) + def ErrBadFormat(self): # Something wrong with format of command + t = 'ERR_BADFORMAT: ' + self.cmd_text + '\n' + print(t) + self.RemoteCtlSend(t) + + def HeartBeat(self): # Called at about 10 Hz by the GUI thread + if self.remote_ctl_connection: + # Monitor the remote connection via periodic heartbeat from Control Head + ts = time.time() + if (ts - self.remote_ctl_heartbeat_ts) > self.remote_ctl_heartbeat_timeout: + print('Closing Remote Control connection: Lost HEARTBEAT from Control Head') + self.RemoteCtlClose(True) + else: + # Continually try to connect with Control Head + self.RemoteCtlOpen() + + def FastHeartBeat(self): # Called frequently by the GUI thread + """This is the remote slave processing loop, and is called frequently. It reads and satisfies requests.""" + if not self.remote_ctl_connection: + return + try: # Read any data from the socket + text = self.remote_ctl_connection.recv(1024) + except: + #traceback.print_exc() + return + else: # We got some characters + if not isinstance(text, str): + text = text.decode('utf-8') + self.received += text + if not '\n' in self.received: # A complete command ending with newline is not available + return + while '\n' in self.received: # At least one complete command ending with newline *is* available + cmd_text, self.received = self.received.split('\n', 1) # Split off the command, save any further characters + cmd_text = cmd_text.strip() # Here is our command + if not cmd_text: + continue + self.cmd_text = cmd_text + args = cmd_text.split(';') # Split at ';' because some control names have blanks + command = args[0] + params = args[1:] + # TOKEN + if self.token: + if command == "TOKEN": + passw = self.app.local_conf.globals.get("remote_radio_password", "") + passw = passw.strip() + if not passw: + self.RemoteCtlSend("TOKEN_MISSING\n") + print ("Error: Missing password on remote radio") + continue + H = hmac.new(passw.encode('utf-8'), self.token.encode('utf-8'), 'sha3_256') + del passw + if hmac.compare_digest(H.hexdigest(), args[1]): + self.token = None + print ("Security challenge passed", args[2]) + self.control_head_data_width = int(args[2]) + self.RemoteCtlSend("TOKEN_OK\n") + self.remote_ctl_heartbeat_ts = time.time() + QS.start_remote_radio_remote_sound(self.control_head_ip, self.remote_radio_sound_port, + self.graph_data_port, self.control_head_data_width) + else: + time.sleep(1) + elif time.time() - self.token_time > 5: + self.RemoteCtlSend("TOKEN_BAD\n") + self.RemoteCtlClose(True) + print ("Security failed") + continue + # Check for Quit and Heartbeat before any other commands + if command == 'QUIT': + print('Closing Remote Control connection: QUIT from Control Head') + # NOTE: Do not send 'Q' to Control Head; sendall() will fail because Control Head already disconnected + self.RemoteCtlClose(False) + continue + # HEARTBEAT + if command == 'HEARTBEAT': + self.remote_ctl_heartbeat_ts = time.time() + continue + # Ignore the On/Off button, Help buttons, Small window pop buttons + if command in ("On", "..", "bandBtnGroup", "screenBtnGroup", "modeButns", "Scope", "Config", "RX Filter", "Help"): + continue + if DEBUG: print("Remote receive:", cmd_text) + # Look for radio buttons + if self.ProcessRadioBtn(command, self.cmd_text): + continue + # buttons in idName2Button + btn = self.app.idName2Button.get(args[0], None) + if btn: + #print ("Slave process button", cmd_text, btn.__class__) + value = int(args[1]) + btn.SetIndex(value, True) + continue + # controls in midiControls + if command in self.app.midiControls: + ctrl, func = self.app.midiControls[command] + #print ("Slave Process control", cmd_text, ctrl.__class__, func) + value = int(args[1]) + if isinstance(ctrl, WrapSlider): + ctrl.ChangeSlider(value) + else: + ctrl.SetValue(value) + func() + continue + # FREQ + if command == 'FREQ': + freq, vfo, source, band, rxFreq, var_decim_index = args[1:] + freq = int(freq) + vfo = int(vfo) + rxFreq = int(rxFreq) + if rxFreq == self.app.rxFreq: + rxFreq = None + var_decim_index = int(var_decim_index) + self.app.ChangeHwFrequency(freq - vfo, vfo, source, band, None, rxFreq) + if source == "NewDecim": + self.app.config_screen.config.btn_decimation.SetSelection(var_decim_index) + sample_rate = self.VarDecimSet(var_decim_index) + self.app.OnBtnDecimation(rate=sample_rate) + # Json function call + elif command == 'JsonAppFunc': + # Call the function self.app. + func with the specified arguments + # func = "%s.SetLabel" % self.idName + # application.Hardware.RemoteCtlSend(f'JsonAppFunc;{json.dumps((func, label, do_cmd, direction))}\n') + jargs = json.loads(params[0]) + pyobj = self.app + for nam in jargs[0].split('.'): + pyobj = getattr(pyobj, nam) + pyobj(*jargs[1:]) + # AGC and Squelch levels + elif command == 'AGCSQLCH': + ctrl = self.app.midiControls["AGCSlider"][0] + ctrl.SetSlider(value_off=int(args[1]), value_on=int(args[2])) + self.app.levelSquelch = int(args[3]) + self.app.levelSquelchSSB = int(args[4]) + self.app.split_offset = int(args[5]) + # CW Keying + elif command == 'CW': + ts = time.time() + if len(params) < 2: + self.ErrParam() + return + if params[0] in '01': + keydown = int(params[0]) + else: + print('Bad keydown value in CW command:', params[0]) + self.ErrParam() + return + cw_event_ts = float(params[1]) / 1000.0 # int msecs to float secs + if cw_event_ts == 0.0: + if keydown != 1: + # 'CW 0 0' == "Force Stop of CW"; clear all queued CW commands, and force CW key up + print('Forcing stop of CW') + while len(self.cw_event_queue): + self.cw_event_queue.popleft() + self.cw_next_event_ts = None + self.cw_next_keydown = None + self.cw_key_down = 0 + QS.set_remote_cwkey(0) + else: + # Begin new cw phrase; any prior phrase should be done by now. + # Set up first cw event to be ready to execute. + self.cw_begin_phrase_ts = ts + self.cw_delay_secs + cw_new_event_ts = self.cw_begin_phrase_ts + cw_event_ts + if not self.cw_next_event_ts: + self.cw_next_event_ts = cw_new_event_ts + self.cw_next_keydown = keydown + if DEBUG_CW_JITTER: print(f'{ts:10.4f} setting: {keydown} {cw_event_ts:2.3f} {cw_new_event_ts:10.4f}') + else: + self.cw_event_queue.append((cw_new_event_ts, keydown)) + if DEBUG_CW_JITTER: print(f'{ts:10.4f} queing: {keydown} {cw_event_ts:2.3f} {cw_new_event_ts:10.4f}') + # Menu + elif command == 'MENU': + menu_name, item_text, checked = args[1:] + if item_text == 'Reverse Rx and Tx': + continue # No need to call handler, as rxFreq and txFreq are already handled + menu = getattr(self.app, menu_name) + nid = menu.item_text2id[item_text] + menu_item = menu.FindItemById(nid) + if menu_item.IsCheckable(): + menu_item.Check(int(checked)) + menu.Handler(None, nid) + else: + t = 'ERR_UNRECOGNIZED_CMD: %s\n' % cmd_text + print(t) + self.RemoteCtlSend(t) + continue + + def PollCwKey(self): # Called periodically at HW Poll usec period (typ. 50-200 Hz) by the sound thread + cw_queue_len = len(self.cw_event_queue) + if self.cw_next_event_ts or cw_queue_len > 0: + # We have at least one CW event. If it's time to do so, set the next CW key down/up, look for next CW event. + ts = time.time() + if DEBUG_CW_JITTER > 1: print(f'{ts:10.4f}') + if not self.cw_next_event_ts: + # Nothing "on deck", but there is something on the cw event queue, so pop it off queue and put it "on deck". + self.cw_next_event_ts, self.cw_next_keydown = self.cw_event_queue.popleft() + if DEBUG_CW_JITTER: print(f'{ts:10.4f} queue len: {cw_queue_len}, popping: {self.cw_next_keydown} {self.cw_next_event_ts:10.4f}') + if ts >= self.cw_next_event_ts: + if DEBUG_CW_JITTER: print(f'{ts:10.4f} set_remote_cwkey: {self.cw_next_keydown}') + QS.set_remote_cwkey(self.cw_next_keydown) + self.cw_key_down = self.cw_next_keydown + cw_queue_len = len(self.cw_event_queue) + if cw_queue_len > 0: + self.cw_next_event_ts, self.cw_next_keydown = self.cw_event_queue.popleft() + if DEBUG_CW_JITTER: print(f'{ts:10.4f} queue len: {cw_queue_len}, popping: {self.cw_next_keydown} {self.cw_next_event_ts:10.4f}') + else: + self.cw_next_event_ts = None + self.cw_next_keydown = None + + def StopTransmit(self): + # TODO: Add code for modes other than CW + while len(self.cw_event_queue): + self.cw_event_queue.popleft() + self.cw_next_event_ts = None + self.cw_next_keydown = None + self.cw_key_down = 0 + QS.set_remote_cwkey(0) + + def ProcessRadioBtn(self, command, cmd_text): + # Large and Small format screens send different button events for radio buttons. + # Band buttons: + if command in self.conf.BandList: + self.app.bandBtnGroup.SetLabel(command, True) + return True # We processed this command + if command in ("Audio", "Time"): + self.app.bandBtnGroup.SetLabel(command, True) + return True # We processed this command + Mode = self.app.modeButns.SetLabel + Screen = self.app.screenBtnGroup.SetLabel + # Mode buttons: process both formats: + if cmd_text in ("CW U/L;0", "CWL;0", "CWL;1"): + Mode("CWL", True) + elif cmd_text in ("CW U/L;1", "CWU;0", "CWU;1"): + Mode("CWU", True) + elif cmd_text in ("SSB U/L;0", "LSB;0", "LSB;1"): + Mode("LSB", True) + elif cmd_text in ("SSB U/L;1", "USB;0", "USB;1"): + Mode("USB", True) + elif cmd_text in ("AM;0", "AM;1"): + Mode("AM", True) + elif cmd_text in ("FM;0", "FM;1"): + Mode("FM", True) + elif cmd_text in ("DGT;0", "DGT-U;0", "DGT-U;1"): + Mode("DGT-U", True) + elif cmd_text in ("DGT;1", "DGT-L;0", "DGT-L;1"): + Mode("DGT-L", True) + elif cmd_text in ("DGT;2", "DGT-FM;0", "DGT-FM;1"): + Mode("DGT-FM", True) + elif cmd_text in ("DGT;3", "DGT-IQ;0", "DGT-IQ;1"): + Mode("DGT-IQ", True) + elif cmd_text in ("FDV;0", "FDV-U;0", "FDV-U;1"): + Mode("FDV-U", True) + elif cmd_text in ("FDV;1", "FDV-L;0", "FDV-L;1"): + Mode("FDV-L", True) + # Screen buttons: process both formats: + # Due to ambiguous received commands, the setting is always "Graph" or "WFall" without "P1 or "P2". + # The P1 and P2 are handled at the control head so this shouldn't matter. + elif cmd_text in ("Graph;0", "Graph;0", "Graph;1"): + Screen("Graph", True) + elif cmd_text in ("Graph;1", "GraphP1;0", "GraphP1;1"): + Screen("Graph", True) + elif cmd_text in ("Graph;2", "GraphP2;0", "GraphP2;1"): + Screen("Graph", True) + elif cmd_text in ("WFall;0", "WFall;0", "WFall;1"): + Screen("WFall", True) + elif cmd_text in ("WFall;1", "WFallP1;0", "WFallP1;1"): + Screen("WFall", True) + elif cmd_text in ("WFall;2", "WFallP2;0", "WFallP2;1"): + Screen("WFall", True) + else: + return False + return True # We processed this command diff --git a/ac2yd/remote_hermes.py b/ac2yd/remote_hermes.py new file mode 100644 index 0000000..38b0590 --- /dev/null +++ b/ac2yd/remote_hermes.py @@ -0,0 +1,25 @@ +# This provides access to a remote radio. See ac2yd/remote_common.py and .pdf files for documentation. + +from hermes.quisk_hardware import Hardware as Radio +from ac2yd.remote_common import Remot + +class Hardware(Remot, Radio): + def __init__(self, app, conf): + Radio.__init__(self, app, conf) + Remot.__init__(self, app, conf) + def open(self): + Remot.open(self) + return "Server: " + Radio.open(self) + def close(self): + Remot.close(self) + Radio.close(self) + def HeartBeat(self): + Remot.HeartBeat(self) + Radio.HeartBeat(self) + if self.app.remote_control_slave: + widg = self.app.bottom_widgets + t = "HL2_TEMP;%s;%s;%s;%s\n" % ( + widg.text_temperature.GetLabel(), widg.text_pa_current.GetLabel(), + widg.text_fwd_power.GetLabel(), widg.text_swr.GetLabel()) + self.RemoteCtlSend(t) + diff --git a/ac2yd/remote_hiqsdr.py b/ac2yd/remote_hiqsdr.py new file mode 100644 index 0000000..393ab92 --- /dev/null +++ b/ac2yd/remote_hiqsdr.py @@ -0,0 +1,18 @@ +# This provides access to a remote radio. See ac2yd/remote_common.py and .pdf files for documentation. + +from hiqsdr.quisk_hardware import Hardware as Radio +from ac2yd.remote_common import Remot + +class Hardware(Remot, Radio): + def __init__(self, app, conf): + Radio.__init__(self, app, conf) + Remot.__init__(self, app, conf) + def open(self): + Remot.open(self) + return "Server: " + Radio.open(self) + def close(self): + Remot.close(self) + Radio.close(self) + def HeartBeat(self): + Remot.HeartBeat(self) + Radio.HeartBeat(self) diff --git a/ac2yd/remote_softrock.py b/ac2yd/remote_softrock.py new file mode 100644 index 0000000..9cf3db1 --- /dev/null +++ b/ac2yd/remote_softrock.py @@ -0,0 +1,19 @@ +# This provides access to a remote radio. See ac2yd/remote_common.py and .pdf files for documentation. + +from softrock.hardware_usb import Hardware as Radio +from ac2yd.remote_common import Remot + +class Hardware(Remot, Radio): + def __init__(self, app, conf): + Radio.__init__(self, app, conf) + Remot.__init__(self, app, conf) + def open(self): + Remot.open(self) + return "Server: " + Radio.open(self) + def close(self): + Remot.close(self) + Radio.close(self) + def HeartBeat(self): + Remot.HeartBeat(self) + Radio.HeartBeat(self) + diff --git a/afedrinet/SOURCE.txt b/afedrinet/SOURCE.txt new file mode 100644 index 0000000..7e70a2e --- /dev/null +++ b/afedrinet/SOURCE.txt @@ -0,0 +1,3 @@ +The files in this afedrinet directory came from http://www.afedri-sdr.com. Only slight +modifications were made by N2ADR. Thanks Alex!!! +N2ADR: Changes made 26 January 2019. diff --git a/afedrinet/__init__.py b/afedrinet/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/afedrinet/__init__.py @@ -0,0 +1 @@ +# diff --git a/afedrinet/af_comp.bat.makeit b/afedrinet/af_comp.bat.makeit new file mode 100644 index 0000000..5ab7034 --- /dev/null +++ b/afedrinet/af_comp.bat.makeit @@ -0,0 +1 @@ +gcc -o afedrinet_io.pyd --shared afedrinet_io.c ../is_key_down.c ../import_quisk_api.c -O3 -I"../" -I"C:\Programs\Python27\include" -L"C:\Programs\Python27\libs" -lws2_32 -lpython27 \ No newline at end of file diff --git a/afedrinet/afe_library b/afedrinet/afe_library new file mode 100644 index 0000000..ef9a493 --- /dev/null +++ b/afedrinet/afe_library @@ -0,0 +1,4 @@ +#!/bin/bash +#replace include path and library name to right one for your system +# for example for Python 2.7 it will look like -I"/usr/include/python2.7/" -lpython2.7 +gcc -o afedrinet_io.so --shared afedrinet_io.c ../is_key_down.c ../import_quisk_api.c -fPIC -O3 -I"../" -I"/usr/include/python2.7/" -lpython2.7 diff --git a/afedrinet/afe_library.mac b/afedrinet/afe_library.mac new file mode 100644 index 0000000..30eb5a0 --- /dev/null +++ b/afedrinet/afe_library.mac @@ -0,0 +1,5 @@ +#!/bin/bash +#replace include path and library name to right one for your system +# for example for Python 2.7 it will look like -I"/usr/include/python2.7/" -lpython2.7 +#/usr/local/Cellar/python/2.7.10_2/Frameworks/Python.framework/Versions/2.7/lib/python2.7 +gcc -o afedrinet_io.so --shared afedrinet_io.c ../is_key_down.c ../import_quisk_api.c -I"../" -I"/opt/local/Library/Frameworks/Python.framework/Versions/2.7/include/python2.7/" -L"/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/" -lpython2.7 diff --git a/afedrinet/afedri.py b/afedrinet/afedri.py new file mode 100644 index 0000000..ede3027 --- /dev/null +++ b/afedrinet/afedri.py @@ -0,0 +1,219 @@ +#!/usr/bin/python + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +################################################## +# AFEDRI Class module +# Title: afedri.py +# Author: k3it + +# Adopted to work with Quisk +# by 4Z5LV +# Last Changes: Sat Feb 02 2013 +# Version: 2.2 + +# Adapted to work with Python3 +# by N2ADR +# Last Changes: January 2020 +################################################## + +from socket import * +import wave +import sys +import struct +import time +import datetime +import string +import math + + +class afedri(object): + """ + class definition for the Afedri SDR-NET + """ + def __init__(self,sdr_address="0.0.0.0", sdr_port=50000): + if sdr_address == "0.0.0.0": + __sdr_address,self.sdr_port = self.__discover_afedri() + if __sdr_address is None: + self.s = None + return + else: + __sdr_address = sdr_address + self.sdr_port = sdr_port + + self.s = socket(AF_INET, SOCK_STREAM) + self.s.settimeout(2) + try: + self.s.connect((__sdr_address,self.sdr_port)) + #print ("Established control connection with AFEDRI") + except: + print ("Error connecting to SDR") + ##sys.exit() + self.s.close() + self.s = None + def close(self): + if self.s: + self.s.close() + self.s = None + def set_center_freq(self,target_freq): + if not self.s: return 1 + __next_freq = target_freq + __next_freq = struct.pack(">3) + return __rf_gain + + def set_gain_indx(self,indx): + if not self.s: return 1 + __gain = (indx << 3) + 1 + # special afedri calculation for the gain byte + #__gain = ((__gain+10)/3 << 3) + 1 + __set_gain_cmd = b"\x06\x00\x38\x00\x00" + struct.pack("B",__gain) + self.s.send(__set_gain_cmd) + __data = self.s.recv(6) + __rf_gain = -10 + 3 * (struct.unpack("B",__data[5:6])[0]>>3) + return __rf_gain + + def get_gain(self): + """ + NOT IMPLEMENTED IN AFEDRI?. DON'T USE + """ + if not self.s: return 1 + __get_gain_cmd = b"\x05\x20\x38\x00\x00" + self.s.send(__get_gain_cmd) + __data = self.s.recv(6) + __rf_gain = -10 + 3 * (struct.unpack("B",__data[5:])[0]>>3) + return __rf_gain + + def get_fe_clock(self): + if not self.s: return 1 + __get_lword_cmd = b"\x09\xE0\x02\x55\x00\x00\x00\x00\x00" + __get_hword_cmd = b"\x09\xE0\x02\x55\x01\x00\x00\x00\x00" + self.s.send(__get_lword_cmd) + __data_l = self.s.recv(9) + self.s.send(__get_hword_cmd) + __data_h = self.s.recv(9) + __fe_clock = struct.unpack("',__DISCOVER_SERVER_PORT)) + try: + __msg=self.sin.recv(256,0) + __devname=__msg[5:20] + __devname=__devname.decode('utf-8') + __sn=__msg[21:36] + __sn=__sn.decode('utf-8') + __ip=inet_ntoa(__msg[40:36:-1]) + __port=struct.unpack("= 0.5): + floor_div += 1 +if floor_div < 15: + floor_div = 15 + #print ("Warning: Max supported sampling rate is", math.floor(fe_main_clock_freq / (4 * floor_div))) +elif floor_div > 625: + floor_div = 625 + #print ("Warning: Min supported sampling rate is", math.floor(fe_main_clock_freq / (4 * floor_div))) + +dSR = fe_main_clock_freq / (4 * floor_div) +floor_SR = math.floor(dSR) +if (dSR - floor_SR >= 0.5): + floor_SR += 1 +if floor_SR != samp_rate: + print ("Warning: invalid sample rate selected for the AFEDRI main clock (", fe_main_clock_freq, "Hz )") + print (" setting to the next valid value", samp_rate, " => ", floor_SR) + samp_rate = floor_SR +""" diff --git a/afedrinet/afedrinet_io.c b/afedrinet/afedrinet_io.c new file mode 100644 index 0000000..af179aa --- /dev/null +++ b/afedrinet/afedrinet_io.c @@ -0,0 +1,399 @@ +#include + +#ifdef MS_WINDOWS +#include +#include +#else +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +#ifdef MS_WINDOWS +#define QUISK_SHUT_RD SD_RECEIVE +#define QUISK_SHUT_BOTH SD_BOTH +#else +#include +#include +#define SOCKET int +#define INVALID_SOCKET -1 +#define QUISK_SHUT_RD SHUT_RD +#define QUISK_SHUT_BOTH SHUT_RDWR +#endif + +#include + +#define IMPORT_QUISK_API +#include "quisk.h" +//#include "sdriq.h" + +static SOCKET rx_udp_socket = INVALID_SOCKET; // Socket for receiving ADC samples from UDP +static int rx_udp_started = 0; // Have we received any data yet? +static int rx_udp_read_blocks = 0; // Number of blocks to read for each read call +static double rx_udp_gain_correct = 1; // For decimation by 5, correct by 4096 / 5**5 +static int use_remove_dc=0; // Remove DC from samples +//static int quisk_using_udp = 0; +// This module provides access to the SDR-IQ by RfSpace. It is the source +// for the Python extension module sdriq. It can be used as a model for an +// extension module for other hardware. Read the end of this file for more +// information. This module was written by James Ahlstrom, N2ADR. + +// This module uses the Python interface to import symbols from the parent _quisk +// extension module. It must be linked with import_quisk_api.c. See the documentation +// at the start of import_quisk_api.c. + +// Start of SDR-IQ specific code: +// +#define DEBUG 0 + +// Type field for the message block header; upper 3 bits of byte +#define TYPE_HOST_SET 0 +#define TYPE_HOST_GET (1 << 5) +#define NAME_SIZE 16 + +//#define UDP_BROADCAST +#ifdef UDP_BROADCAST + #define FIRST_IQ_DATA_IDX 20 + #define RX_UDP_SIZE 1044 // Expected size of UDP samples packet +#else + #define RX_UDP_SIZE 1028 // Expected size of UDP samples packet + #define FIRST_IQ_DATA_IDX 4 +#endif +#define BROADCAST_HEADER_SIZE 16 +#define UDP_PROTCOL_ID1 0x04 +#define UDP_PROTCOL_ID2 0x18 + +#ifdef DEBUG_IO +#undef DEBUG_IO +#define DEBUG_IO 1 +#endif + +static PyObject * open_rx_udp(const char * ip, int port) +{ +// const char * ip; +// int port; + char buf[128]; + struct sockaddr_in Addr; + int recvsize; + char optval; +#if DEBUG_IO + int intbuf; +#ifdef MS_WINDOWS + int bufsize = sizeof(int); +#else + socklen_t bufsize = sizeof(int); +#endif +#endif + +#ifdef MS_WINDOWS + WORD wVersionRequested; + WSADATA wsaData; +#endif + +// if (!PyArg_ParseTuple (args, "si", &ip, &port)) +// return NULL; +// port = 50000; +#ifdef MS_WINDOWS + wVersionRequested = MAKEWORD(2, 2); + if (WSAStartup(wVersionRequested, &wsaData) != 0) { + sprintf(buf, "Failed to initialize Winsock (WSAStartup)"); + return PyString_FromString(buf); + } +#endif +// quisk_using_udp = 1; + rx_udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (rx_udp_socket != INVALID_SOCKET) + { + optval=1; + setsockopt( rx_udp_socket, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval) ); + recvsize = 256000; + setsockopt(rx_udp_socket, SOL_SOCKET, SO_RCVBUF, (char *)&recvsize, sizeof(recvsize)); + memset(&Addr, 0, sizeof(Addr)); + Addr.sin_family = AF_INET; + Addr.sin_port = htons(port); + Addr.sin_addr.s_addr = htonl(INADDR_ANY);//inet_addr("192.168.0.8"); +// Addr.sin_addr.S_un.S_addr = inet_addr(ip); +// if (connect(rx_udp_socket, (const struct sockaddr *)&Addr, sizeof(Addr)) != 0) + if (bind(rx_udp_socket, (const struct sockaddr *)&Addr, sizeof(Addr)) != 0) + { + shutdown(rx_udp_socket, QUISK_SHUT_BOTH); + close(rx_udp_socket); + rx_udp_socket = INVALID_SOCKET; + sprintf(buf, "Failed to connect to UDP %s port %u", ip, port); + } + else { + sprintf(buf, "Capture from UDP %s port %u", ip, port); +#if DEBUG_IO + if (getsockopt(rx_udp_socket, SOL_SOCKET, SO_RCVBUF, (char *)&intbuf, &bufsize) == 0) + { + printf("UDP socket receive buffer size %d\n", intbuf); + printf("address %s port %u\n", ip, port); + } + else + printf ("Failure SO_RCVBUF\n"); +#endif + } + } + else { + sprintf(buf, "Failed to open socket"); + } + return PyString_FromString(buf); +} + +static PyObject * close_rx_udp(PyObject * self, PyObject * args) +{ + short msg = 0x7373; // shutdown + + if (!PyArg_ParseTuple (args, "")) + return NULL; + + if (rx_udp_socket != INVALID_SOCKET) { + shutdown(rx_udp_socket, QUISK_SHUT_RD); + send(rx_udp_socket, (char *)&msg, 2, 0); + send(rx_udp_socket, (char *)&msg, 2, 0); + QuiskSleepMicrosec(3000000); + close(rx_udp_socket); + rx_udp_socket = INVALID_SOCKET; + } + rx_udp_started = 0; + +// if (quisk_using_udp) { +// quisk_using_udp = 0; + +#ifdef MS_WINDOWS + WSACleanup(); +#endif +// } + Py_INCREF (Py_None); + return Py_None; +} + +int afedri_read_rx_udp(complex * samp) // Read samples from UDP +{ // Size of complex sample array is SAMP_BUFFER_SIZE + ssize_t bytes; + //int SR = 0; + static int sample_rate = 0; // Sample rate such as 48000, 96000, 192000 + unsigned char buf[1500]; // Maximum Ethernet is 1500 bytes. + static unsigned short seq0; // must be 8 bits + unsigned short seq_curr = 0; +#ifdef MS_WINDOWS + __int32 i, count, nSamples, xr, xi, index; +#else + int32_t i, count, nSamples, xr, xi, index; +#endif + unsigned char * ptxr, * ptxi; + static complex dc_average = 0; // Average DC component in samples + static complex dc_sum = 0; + static int dc_count = 0; + static int dc_key_delay = 0; + + // Data from the receiver is little-endian +// if ( !rx_udp_read_blocks) + if(sample_rate != pt_quisk_sound_state->sample_rate) + { + sample_rate = pt_quisk_sound_state->sample_rate; + // "rx_udp_read_blocks" is the number of UDP blocks to read at once + rx_udp_read_blocks = (int)(pt_quisk_sound_state->data_poll_usec * 1e-6 * sample_rate + 0.5); + rx_udp_read_blocks = (rx_udp_read_blocks + (RX_UDP_SIZE / 12)) / (RX_UDP_SIZE / 6); // 6 bytes per sample + if (rx_udp_read_blocks < 1) + rx_udp_read_blocks = 1; +#if DEBUG_IO + printf("read_rx_udp: rx_udp_read_blocks %d\n", rx_udp_read_blocks); +#endif + } +/* if ( ! rx_udp_gain_correct) { + int dec; + dec = (int)(rx_udp_clock / sample_rate + 0.5); + if ((dec / 5) * 5 == dec) // Decimation by a factor of 5 + rx_udp_gain_correct = 1.31072; + else // Decimation by factors of two + rx_udp_gain_correct = 1.0; + } +*/ + nSamples = 0; + for (count = 0; count < rx_udp_read_blocks; count++) + { // read several UDP blocks +#if DEBUG_IO +// printf("Data RX Process Begin %u\n",count); +#endif + bytes = recv(rx_udp_socket, (char *)buf, RX_UDP_SIZE, 0); // blocking read + if (bytes != RX_UDP_SIZE) { // Known size of sample block + pt_quisk_sound_state->read_error++; +#if DEBUG_IO + printf("read_rx_udp: Bad block size %i\n", (int)bytes); +#endif + continue; + } + // buf[0] is the sequence number + // buf[1] is the status: + // bit 0: key up/down state + // bit 1: set for ADC overrange (clip) + seq_curr = buf[2] | (buf[3] << 8); + if (seq_curr != seq0) { +#if DEBUG_IO + printf("read_rx_udp: Bad sequence want %3d got %3d at block %d of %d\n", + (unsigned int)seq0, (unsigned int)buf[0], count, rx_udp_read_blocks); +#endif + pt_quisk_sound_state->read_error++; + } + seq0 = seq_curr + 1; // Next expected sequence number + // quisk_set_key_down(buf[1] & 0x01); // bit zero is key state + // if (buf[1] & 0x02) // bit one is ADC overrange + // quisk_sound_state.overrange++; + index = FIRST_IQ_DATA_IDX; + ptxr = (unsigned char *)&xr; + ptxi = (unsigned char *)ξ + // convert 24-bit samples to 32-bit samples; int must be 32 bits. + while (index < bytes) + { + xr = xi = 0; + memcpy (ptxr + 2, buf + index, 2); + index += 2; + memcpy (ptxi + 2, buf + index, 2); + index += 2; + samp[nSamples++] = (xr + xi * I) * rx_udp_gain_correct; + xr = xi = 0; + memcpy (ptxr + 2, buf + index, 2); + index += 2; + memcpy (ptxi + 2, buf + index, 2); + index += 2; + samp[nSamples++] = (xr + xi * I) * rx_udp_gain_correct;; + //if (nSamples == 2) printf("%12d %12d\n", xr, xi); + } + } + if (quisk_is_key_down()) { + dc_key_delay = 0; + dc_sum = 0; + dc_count = 0; + } + else if (dc_key_delay < pt_quisk_sound_state->sample_rate) { + dc_key_delay += nSamples; + } + else { + dc_count += nSamples; + for (i = 0; i < nSamples; i++) // Correction for DC offset in samples + dc_sum += samp[i]; + if (dc_count > pt_quisk_sound_state->sample_rate * 2) { + dc_average = dc_sum / dc_count; + dc_sum = 0; + dc_count = 0; + //printf("dc average %lf %lf %d\n", creal(dc_average), cimag(dc_average), dc_count); + //printf("dc polar %.0lf %d\n", cabs(dc_average), + // (int)(360.0 / 2 / M_PI * atan2(cimag(dc_average), creal(dc_average)))); + } + } + if (use_remove_dc) + for (i = 0; i < nSamples; i++) // Correction for DC offset in samples + samp[i] -= dc_average; + +// printf("%u\n", pt_quisk_sound_state->sample_rate); + return nSamples; +} + + + +// End of most AFEDRI specific code. + +/////////////////////////////////////////////////////////////////////////// +// The API requires at least two Python functions for Open and Close, plus +// additional Python functions as needed. And it requires exactly three +// C funcions for Start, Stop and Read samples. Quisk runs in two threads, +// a GUI thread and a sound thread. You must not call the GUI or any Python +// code from the sound thread. You must return promptly from functions called +// by the sound thread. +// +// The calling sequence is Open, Start, then repeated calls to Read, then +// Stop, then Close. + +// Start of Application Programming Interface (API) code: + +// Called to close the sample source; called from the GUI thread. +static PyObject * close_samples(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + close_rx_udp(self, args); + Py_INCREF (Py_None); + return Py_None; +} + +// Called to open the sample source; called from the GUI thread. +static PyObject * open_samples(PyObject * self, PyObject * args) +{ + const char * ip; + int port; +// const char * name; +// char buf[128]; + + if (!PyArg_ParseTuple (args, "si", &ip, &port)) + return NULL; + +// name = QuiskGetConfigString("sdriq_name", "NoName"); +// sdriq_clock = QuiskGetConfigDouble("sdriq_clock", 66666667.0); + +// Record our C-language Start/Stop/Read functions for use by sound.c. + quisk_sample_source(NULL, NULL, &afedri_read_rx_udp); +////////////// + return open_rx_udp(ip, port); // AFEDRI specific +// return PyString_FromString(buf); // return a string message +} + +// Miscellaneous functions needed by the SDR-IQ; called from the GUI thread as +// a result of button presses. + + +// Functions callable from Python are listed here: +static PyMethodDef QuiskMethods[] = { + {"open_samples", open_samples, METH_VARARGS, "Open the AFEDRI SDR-Net."}, + {"close_samples", close_samples, METH_VARARGS, "Close the AFEDRI SDR-Net."}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +#if PY_MAJOR_VERSION < 3 +// Python 2.7: +PyMODINIT_FUNC initafedrinet_io (void) +{ + if (Py_InitModule ("afedrinet_io", QuiskMethods) == NULL) { + printf("Py_InitModule failed!\n"); + return; + } + // Import pointers to functions and variables from module _quisk + if (import_quisk_api()) { + printf("Failure to import pointers from _quisk\n"); + return; //Error + } +} + +// Python 3: +#else +static struct PyModuleDef afedrinet_iomodule = { + PyModuleDef_HEAD_INIT, + "afedrinet_io", + NULL, + -1, + QuiskMethods +} ; + +PyMODINIT_FUNC PyInit_afedrinet_io(void) +{ + PyObject * m; + + m = PyModule_Create(&afedrinet_iomodule); + if (m == NULL) + return NULL; + // Import pointers to functions and variables from module _quisk + if (import_quisk_api()) { + printf("Failure to import pointers from _quisk\n"); + return m; //Error + } + return m; +} +#endif diff --git a/afedrinet/makefile b/afedrinet/makefile new file mode 100644 index 0000000..e0fb199 --- /dev/null +++ b/afedrinet/makefile @@ -0,0 +1,7 @@ + +afedrinet2: + python2 setup.py build_ext --force --inplace + +afedrinet3: + python3 setup.py build_ext --force --inplace + diff --git a/afedrinet/quisk_conf.py b/afedrinet/quisk_conf.py new file mode 100644 index 0000000..a06e908 --- /dev/null +++ b/afedrinet/quisk_conf.py @@ -0,0 +1,61 @@ +# This is a sample quisk_conf.py configuration file for Microsoft Windows. + +# For Windows, your default config file name is "My Documents/quisk_conf.py", +# but you can use a different config file by using -c or --config. Quisk creates +# an initial default config file if there is none. To control Quisk, edit +# "My Documents/quisk_conf.py" using any text editor; for example WordPad (not Notepad). + +# In Windows you can see what sound devices you have, and you can set the Primary +# Device for capture and playback by using Control Panel/Sounds and Audio Devices. +# If you have only one sound device, it should be set as "Primary". If you have +# several, find the names by using Control Panel/Sounds and Audio Devices; for +# example, you may have "SoundMAX HD Audio" in the list for "Sound playback" and +# "Sound recording". To specify this device for capture (recording) or playback, +# enter a unique part of its name using exact upper/lower case. For example: +# name_of_sound_capture = "SoundMAX" +# name_of_sound_play = "SoundMAX" + +# There are many possible options for your config file. Copy the ones you want +# from the master file quisk_conf_defaults.py (but don't change the master file). +# The master config file is located in the site-packages/quisk folder for Python 2.7. + +# This file is Python code and the comment character is "#". To ignore a line, +# start it with "#". To un-ignore a line, remove the "#". Generally you must start +# lines in column one (the left edge) except for logic blocks. +from afedrinet import quisk_hardware # Use different hardware file + +use_rx_udp = 1 # Get ADC samples from UDP +rx_udp_ip = "192.168.0.8" # Sample source IP address +rx_udp_port = 50000 # Sample source UDP port +rx_udp_clock = 79998382 # ADC sample rate in Hertz +#rx_udp_decimation = 8 * 8 * 8 # Decimation from clock to UDP sample rate +#sample_rate = int(float(rx_udp_clock) / rx_udp_decimation + 0.5) # Don't change this +data_poll_usec = 10000 +#sample_rate = 192000 # ADC hardware sample rate in Hertz +sample_rate = 740740 # ADC hardware sample rate in Hertz +playback_rate = 48000 # Radio sound play rate +name_of_sound_capt = ""#AFEDRI-SDR-Net Audio" # Name of soundcard capture hardware device. +name_of_sound_play = "Buil-in Output" # Use the same device for play back. +#name_of_sound_play = "Line 1"#Virtual Audio Cable" # Use the same device for play back. +latency_millisecs = 50 # latency time in milliseconds +display_fraction = 0.92 # The edges of the full bandwidth are not valid +default_rf_gain = 11 +# Select the default screen when Quisk starts: +#default_screen = 'Graph' +default_screen = 'WFall' + +# If you use hardware with a fixed VFO (crystal controlled SoftRock) un-comment the following: +# import quisk_hardware_fixed as quisk_hardware +# fixed_vfo_freq = 7056000 + +# If you use an SDR-IQ for capture, first install the SpectraView software +# that came with the SDR-IQ. This will install the USB driver. Then set these parameters: +# import quisk_hardware_sdriq as quisk_hardware # Use different hardware file +# use_sdriq = 1 # Capture device is the SDR-IQ +# sdriq_name = "SDR-IQ" # Name of the SDR-IQ device to open +# sdriq_clock = 66666667.0 # actual sample rate (66666667 nominal) +# sdriq_decimation = 500 # Must be 360, 500, 600, or 1250 +# sample_rate = int(float(sdriq_clock) / sdriq_decimation + 0.5) # Don't change this +# name_of_sound_capt = "" # We do not capture from the soundcard +# playback_rate = 48000 # Radio sound play rate, default 48000 +# display_fraction = 0.85 # The edges of the full bandwidth are not valid diff --git a/afedrinet/quisk_conf_linux.py b/afedrinet/quisk_conf_linux.py new file mode 100644 index 0000000..ee19885 --- /dev/null +++ b/afedrinet/quisk_conf_linux.py @@ -0,0 +1,54 @@ +# This is a sample quisk_conf.py configuration file for Linux +# For Windows, your default config file name is "/home/user/quisk_conf.py" +# but you can use a different config file by using -c or --config. Quisk creates +# an initial default config file if there is none. To control Quisk, edit +# "/home/user/quisk_conf.py" using any text editor; for example WordPad (not Notepad). +# In Windows you can see what sound devices you have, and you can set the Primary +# Device for capture and playback by using Control Panel/Sounds and Audio Devices. +# If you have only one sound device, it should be set as "Primary". If you have +# several, find the names by using Control Panel/Sounds and Audio Devices; for +# example, you may have "SoundMAX HD Audio" in the list for "Sound playback" and +# "Sound recording". To specify this device for capture (recording) or playback, +# enter a unique part of its name using exact upper/lower case. For example: +# name_of_sound_capture = "SoundMAX" +# name_of_sound_play = "SoundMAX" +# There are many possible options for your config file. Copy the ones you want +# from the master file quisk_conf_defaults.py (but don't change the master file). +# The master config file is located in the site-packages/quisk folder for Python 2.7. +# This file is Python code and the comment character is "#". To ignore a line, +# start it with "#". To un-ignore a line, remove the "#". Generally you must start +# lines in column one (the left edge) except for logic blocks. +from afedrinet import quisk_hardware # Use different hardware file +use_rx_udp = 1 # Get ADC samples from UDP +rx_udp_ip = "192.168.0.8" # Sample source IP address +rx_udp_port = 50000 # Sample source UDP port +rx_udp_clock = 80000000 # ADC sample rate in Hertz +#rx_udp_decimation = 8 * 8 * 8 # Decimation from clock to UDP sample rate +#sample_rate = int(float(rx_udp_clock) / rx_udp_decimation + 0.5) # Don't change this +data_poll_usec = 10000 +#sample_rate = 192000 # ADC hardware sample rate in Hertz +sample_rate = 740740 # ADC hardware sample rate in Hertz +playback_rate = 48000 # Radio sound play rate +name_of_sound_capt = ""#AFEDRI-SDR-Net Audio" # Name of soundcard capture hardware device. +name_of_sound_play = "portaudiodefault"#hw:0,0" # Use the same device for play back. +#name_of_sound_play = "Line 1"#Virtual Audio Cable" # Use the same device for play back. +latency_millisecs = 50 # latency time in milliseconds +display_fraction = 0.92 # The edges of the full bandwidth are not valid +default_rf_gain = 14 +# Select the default screen when Quisk starts: +#default_screen = 'Graph' +default_screen = 'WFall' +# If you use hardware with a fixed VFO (crystal controlled SoftRock) un-comment the following: +# import quisk_hardware_fixed as quisk_hardware +# fixed_vfo_freq = 7056000 +# If you use an SDR-IQ for capture, first install the SpectraView software +# that came with the SDR-IQ. This will install the USB driver. Then set these parameters: +# import quisk_hardware_sdriq as quisk_hardware # Use different hardware file +# use_sdriq = 1 # Capture device is the SDR-IQ +# sdriq_name = "SDR-IQ" # Name of the SDR-IQ device to open +# sdriq_clock = 66666667.0 # actual sample rate (66666667 nominal) +# sdriq_decimation = 500 # Must be 360, 500, 600, or 1250 +# sample_rate = int(float(sdriq_clock) / sdriq_decimation + 0.5) # Don't change this +# name_of_sound_capt = "" # We do not capture from the soundcard +# playback_rate = 48000 # Radio sound play rate, default 48000 +# display_fraction = 0.85 # The edges of the full bandwidth are not valid diff --git a/afedrinet/quisk_conf_mac.py b/afedrinet/quisk_conf_mac.py new file mode 100644 index 0000000..ee19885 --- /dev/null +++ b/afedrinet/quisk_conf_mac.py @@ -0,0 +1,54 @@ +# This is a sample quisk_conf.py configuration file for Linux +# For Windows, your default config file name is "/home/user/quisk_conf.py" +# but you can use a different config file by using -c or --config. Quisk creates +# an initial default config file if there is none. To control Quisk, edit +# "/home/user/quisk_conf.py" using any text editor; for example WordPad (not Notepad). +# In Windows you can see what sound devices you have, and you can set the Primary +# Device for capture and playback by using Control Panel/Sounds and Audio Devices. +# If you have only one sound device, it should be set as "Primary". If you have +# several, find the names by using Control Panel/Sounds and Audio Devices; for +# example, you may have "SoundMAX HD Audio" in the list for "Sound playback" and +# "Sound recording". To specify this device for capture (recording) or playback, +# enter a unique part of its name using exact upper/lower case. For example: +# name_of_sound_capture = "SoundMAX" +# name_of_sound_play = "SoundMAX" +# There are many possible options for your config file. Copy the ones you want +# from the master file quisk_conf_defaults.py (but don't change the master file). +# The master config file is located in the site-packages/quisk folder for Python 2.7. +# This file is Python code and the comment character is "#". To ignore a line, +# start it with "#". To un-ignore a line, remove the "#". Generally you must start +# lines in column one (the left edge) except for logic blocks. +from afedrinet import quisk_hardware # Use different hardware file +use_rx_udp = 1 # Get ADC samples from UDP +rx_udp_ip = "192.168.0.8" # Sample source IP address +rx_udp_port = 50000 # Sample source UDP port +rx_udp_clock = 80000000 # ADC sample rate in Hertz +#rx_udp_decimation = 8 * 8 * 8 # Decimation from clock to UDP sample rate +#sample_rate = int(float(rx_udp_clock) / rx_udp_decimation + 0.5) # Don't change this +data_poll_usec = 10000 +#sample_rate = 192000 # ADC hardware sample rate in Hertz +sample_rate = 740740 # ADC hardware sample rate in Hertz +playback_rate = 48000 # Radio sound play rate +name_of_sound_capt = ""#AFEDRI-SDR-Net Audio" # Name of soundcard capture hardware device. +name_of_sound_play = "portaudiodefault"#hw:0,0" # Use the same device for play back. +#name_of_sound_play = "Line 1"#Virtual Audio Cable" # Use the same device for play back. +latency_millisecs = 50 # latency time in milliseconds +display_fraction = 0.92 # The edges of the full bandwidth are not valid +default_rf_gain = 14 +# Select the default screen when Quisk starts: +#default_screen = 'Graph' +default_screen = 'WFall' +# If you use hardware with a fixed VFO (crystal controlled SoftRock) un-comment the following: +# import quisk_hardware_fixed as quisk_hardware +# fixed_vfo_freq = 7056000 +# If you use an SDR-IQ for capture, first install the SpectraView software +# that came with the SDR-IQ. This will install the USB driver. Then set these parameters: +# import quisk_hardware_sdriq as quisk_hardware # Use different hardware file +# use_sdriq = 1 # Capture device is the SDR-IQ +# sdriq_name = "SDR-IQ" # Name of the SDR-IQ device to open +# sdriq_clock = 66666667.0 # actual sample rate (66666667 nominal) +# sdriq_decimation = 500 # Must be 360, 500, 600, or 1250 +# sample_rate = int(float(sdriq_clock) / sdriq_decimation + 0.5) # Don't change this +# name_of_sound_capt = "" # We do not capture from the soundcard +# playback_rate = 48000 # Radio sound play rate, default 48000 +# display_fraction = 0.85 # The edges of the full bandwidth are not valid diff --git a/afedrinet/quisk_hardware.py b/afedrinet/quisk_hardware.py new file mode 100644 index 0000000..c352e49 --- /dev/null +++ b/afedrinet/quisk_hardware.py @@ -0,0 +1,94 @@ +# Please do not change this hardware control module. +# It provides support for the SDR-IQ by RfSpace. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +#import _quisk as QS +import os,sys +#sys.path.append('./afedri') +try: + import afedrinet_io as AF + from sdr_control import * +except: + from afedrinet import afedrinet_io as AF + from afedrinet.sdr_control import * +from ctypes import * +os.environ['PATH'] = os.path.dirname(__file__) + ';' + os.environ['PATH'] +#from quisk import App as parent +from quisk_hardware_model import Hardware as BaseHardware +#from quisk import * + +class Hardware(BaseHardware): + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.local_tune = 2 + self.index = 0 + self.clock = 80000000 + self.old_LO_freq = 0 + self.rf_gain_labels = ('RF -10','RF -7','RF -4','RF -1','+2','+5','+8','+11','+14','+17','+20','+23','+26','+29','+32','+35') + self.conf = conf + self.plugin = Control(self.conf.rx_udp_ip, self.conf.rx_udp_port) + self.app = app + self.decimations = [] # supported decimation rates + for dec in (53333, 96000, 133333, 185185, 192000, 370370, 740740, 1333333): + self.decimations.append(dec) + def open(self): + self.plugin.OpenHW() # Return a config message + RF_Gain_idx = int((10 + self.conf.default_rf_gain) / 3) + if not 0 <= RF_Gain_idx < len(self.rf_gain_labels): + RF_Gain_idx = 0 + self.plugin.SetAttenuator(RF_Gain_idx) + self.app.BtnRfGain.SetIndex(RF_Gain_idx) + #print ("RF Gain %i" % self.conf.default_rf_gain) + return AF.open_samples(self.conf.rx_udp_ip, self.conf.rx_udp_port) + def close(self): + self.plugin.CloseHW() + def OnButtonRfGain(self, event): + btn = event.GetEventObject() + n = btn.index + if n > -1 or n < 16 : + self.plugin.SetAttenuator(n) + else: + print ('Unknown RfGain') + def ChangeFrequency(self, tune, vfo, source='', band='', event=None): + self.local_tune = tune + if vfo: + self.plugin.SetHWLO(vfo) + return tune, vfo + def ReturnFrequency(self): # Return the current tuning and VFO frequency + return (None, None) # Return LO frequency + def GetFirmwareVersion(self): + return 226 + def HeartBeat(self): +# self.PrintStatus('Start', 'AFEDRI') + return + def VarDecimGetChoices(self): # Return a list/tuple of strings for the decimation control. + l = [] # a list of sample rates + for dec in self.decimations: + l.append(str( dec )) + return l + def VarDecimGetLabel(self): # return a text label for the control + return "Sample rate sps" + def VarDecimGetIndex(self): # return the current index + return self.index + def VarDecimSet(self, index=None): # set decimation, return sample rate + if index is None: # initial call to set decimation before the call to open() + rate = self.application.vardecim_set # May be None or from different hardware + try: + dec = rate #int(float(self.conf.rx_udp_clock / rate + 0.5)) + self.index = self.decimations.index(dec) + except: + try: + self.index = self.decimations.index(self.conf.sample_rate) + except: + self.index = 0 + else: + self.index = index + dec = self.decimations[self.index] + self.plugin.SetHWSR(dec) # Return a config message + return dec + + + diff --git a/afedrinet/readme.txt b/afedrinet/readme.txt new file mode 100644 index 0000000..3d25238 --- /dev/null +++ b/afedrinet/readme.txt @@ -0,0 +1,38 @@ +Hello All! +I want to publish here some information about possibility to use Quisk application on MAC computers running MAC OSX. +It is for experienced MAC OS users that know what is Terminal and command shell tools and how to compile executable from source code: + +1. You should download Quisk package from author's web page +2. Xcode SDK and command tools +3. Install macports to manage ports that required to be used with Quisk +4. You should install the next packages: + python27, py27-wxpython-2.8, fftw-3.0 , portaudio, pulseaudio and probably some additional +5. Be ready that packages installation will take long time. Select macports python as default python executable: +>> sudo port select --set python python27 +6. After you will extract Quisk files from archive to separate directory you must enter Quisk directory and compile Quisk for your system running next command: +>> make macports +7. After installation to use Quisk with AFEDRi SDR, you have to download additional packages: afedriusb , afedrinet extract contains of archive to separated folders afedriusb and afedrinet in main Quisk directory. +8. For network connected AFEDRI you must enter afedrinet folder and compile SDR support library running afe_library script, before you can run this script you must modify path to python library and include folders in this script, for example on my MAC it looks like: +############################################################### +gcc -o afedrinet_io.so --shared afedrinet_io.c ../is_key_down.c ../import_quisk_api.c -I"../" -I"/opt/local/Library/Frameworks/Python.framework/Versions/2.7/include/python2.7/" -L"/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/" -lpython2.7 +################################################################### +9. For USB connection you must download the sdr_commander v1.22 + Extract archive to separate folder and compile source code. + After successful compilation you will get executable sdr_commander, you must + copy it to quick/afedriusb folder +10. + 10. Edit qusik_conf.py file to define correct sound device (card) name, copy this file as .quisk_conf.py to user's home directory. + For example for portaudio devices it will look like this one: + + ################################################################# + from afedriusb import quisk_hardware + sample_rate = 185185 + name_of_sound_capt = "portaudio#2" + name_of_sound_play = "portaudiodefault" + latency_millisecs = 150 + default_rf_gain = 14 + default_screen = 'WFall' + playback_rate = 48000 + default 48000 + ########################################################## + diff --git a/afedrinet/sdr_control.py b/afedrinet/sdr_control.py new file mode 100644 index 0000000..d43b101 --- /dev/null +++ b/afedrinet/sdr_control.py @@ -0,0 +1,38 @@ +#!/usr/bin/python # This is client.py file + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import os,sys +os.environ['PATH'] = os.path.dirname(__file__) + ';' + os.environ['PATH'] + +import afedrinet +import afedrinet.afedri + +class Control: + def __init__(self, sdr_address="192.168.0.8", sdr_port=50000): + self.hw = afedrinet.afedri.afedri(sdr_address, sdr_port) + if self.hw.s is None: # Failure to find the hardware + self.hw = None + def OpenHW(self): + if not self.hw: return + data = self.hw.get_sdr_name() + print (data[4:]) + self.hw.start_capture() + def CloseHW(self): + if not self.hw: return + self.hw.stop_capture() + self.hw.close # Close the socket when done + def SetHWLO(self, vfo): + if not self.hw: return + self.hw.set_center_freq(vfo) + def SetHWSR(self, sample_rate): + if not self.hw: return + self.hw.set_samp_rate(sample_rate) + print ("Sample Rate %i" % sample_rate) + def SetAttenuator(self, indx): + if not self.hw: return + self.hw.set_gain_indx(indx) + + diff --git a/afedrinet/setup.py b/afedrinet/setup.py new file mode 100644 index 0000000..b2bd2d4 --- /dev/null +++ b/afedrinet/setup.py @@ -0,0 +1,49 @@ +from distutils.core import setup, Extension +import sys + +# Afedri hardware support added by Alex, Alex@gmail.com + +if sys.platform == "win32": + Modules = [ + Extension ('afedrinet.afedrinet_io', + libraries = ['WS2_32'], + sources = ['../import_quisk_api.c', '../is_key_down.c', 'afedrinet_io.c'], + include_dirs = ['.', '..'], + ) + ] +else: + Modules = [ + Extension ('afedrinet.afedrinet_io', + libraries = ['m'], + sources = ['../import_quisk_api.c', '../is_key_down.c', 'afedrinet_io.c'], + include_dirs = ['.', '..'], + ) + ] + +setup (name = 'afedrinet_io', + version = '0.1', + description = 'Afedri', + long_description = "Afedri.", + author = 'Alex', + author_email = 'Alex@gmail.com', + #url = 'http://', + download_url = 'http://james.ahlstrom.name/quisk/', + packages = ['afedrinet.afedrinet_io'], + package_dir = {'afedrinet' : '.'}, + ext_modules = Modules, + classifiers = [ + 'Development Status :: 6 - Mature', + 'Environment :: X11 Applications', + 'Environment :: Win32 (MS Windows)', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Natural Language :: English', + 'Operating System :: POSIX :: Linux', + 'Operating System :: Microsoft :: Windows', + 'Programming Language :: Python', + 'Programming Language :: C', + 'Topic :: Communications :: Ham Radio', + ], +) + + diff --git a/afedrinet/test.py b/afedrinet/test.py new file mode 100644 index 0000000..f1c179b --- /dev/null +++ b/afedrinet/test.py @@ -0,0 +1,16 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import os,sys +from ctypes import * +os.environ['PATH'] = os.path.dirname(__file__) + ';' + os.environ['PATH'] +from sdr_control import * +#import sdr_control + +#class self(control) +cnt = Control() +cnt.OpenHW() +cnt.SetHWLO(28050000) +cnt.SetHWSR(740740) +cnt.CloseHW() diff --git a/configure.py b/configure.py new file mode 100644 index 0000000..c38f215 --- /dev/null +++ b/configure.py @@ -0,0 +1,4294 @@ + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import sys, wx, wx.lib, os, re, pickle, traceback, json +# Quisk will alter quisk_conf_defaults to include the user's config file. +import quisk_conf_defaults as conf +import _quisk as QS +from quisk_widgets import QuiskPushbutton, QuiskCheckbutton, QuiskBitField, SliderBoxH, SliderBoxHH +from quisk_widgets import wxVersion +if wxVersion in ('2', '3'): + import wx.combo as wxcombo +else: + wxcombo = wx # wxPython Phoenix +try: + from soapypkg import soapy +except: + soapy = None + +# Settings is [ +# 0: Radio_requested, a string radio name or "Ask me" or "ConfigFileRadio" +# 1: Radio in use and last used, a string radio name or "ConfigFileRadio" +# 2: List of radio names +# 3: Parallel list of radio dicts. These are all the parameters for the corresponding radio. In +# general, they are a subset of all the parameters listed in self.sections and self.receiver_data[radio_name]. +# 4: Global data common to all radios. This is similar to the radio dicts. Available as local_conf.globals. +# ] + +# radio_dict is a dictionary of variable names and text values for each radio including radio ConfigFileRadio. +# Only variable names from the specified radio and all sections are included. The data comes from the JSON file, and +# may be missing recently added config file items. Use GetValue() to get a configuration datum. + +# local_conf is the single instance of class Configuration. conf is the configuration data from quisk_conf_defaults as +# over-writen by JSON data in radio_dict. Items in the radio_dict are generally strings. We convert these strings to Python +# integers, floats, etc. and write them to conf. + +# The format for data items is read from quisk_conf_defaults.py, but there are data items not in this file. The +# dictionary name2format has formats and defaults for these additional items. +# The value is a tuple (format name, default value). +name2format = { + "digital_rx1_name":('text', ''), "digital_rx2_name":('text', ''), "digital_rx3_name":('text', ''), + "digital_rx4_name":('text', ''), "digital_rx5_name":('text', ''), "digital_rx6_name":('text', ''), + "digital_rx7_name":('text', ''), "digital_rx8_name":('text', ''), "digital_rx9_name":('text', ''), + "win_digital_rx1_name":('text', ''), "win_digital_rx2_name":('text', ''), "win_digital_rx3_name":('text', ''), + "win_digital_rx4_name":('text', ''), "win_digital_rx5_name":('text', ''), "win_digital_rx6_name":('text', ''), + "win_digital_rx7_name":('text', ''), "win_digital_rx8_name":('text', ''), "win_digital_rx9_name":('text', ''), + "lin_digital_rx1_name":('text', ''), "lin_digital_rx2_name":('text', ''), "lin_digital_rx3_name":('text', ''), + "lin_digital_rx4_name":('text', ''), "lin_digital_rx5_name":('text', ''), "lin_digital_rx6_name":('text', ''), + "lin_digital_rx7_name":('text', ''), "lin_digital_rx8_name":('text', ''), "lin_digital_rx9_name":('text', ''), +} + +# Increasing the software version will display a message to re-read the soapy device. +soapy_software_version = 3 + +def FormatKhz(dnum): # Round to 3 decimal places; remove ending ".000" + t = "%.3f" % dnum + if t[-4:] == '.000': + t = t[0:-4] + return t + +def FormatMHz(dnum): # Pretty print in MHz + t = "%.6f" % dnum + for i in range(3): + if t[-1] == '0': + t = t[0:-1] + else: + break + return t + +def SortKey(x): + try: + k = float(x) + except: + k = 0.0 + return k + +class Configuration: + def __init__(self, app, AskMe=False, Radio=''): # Called first + global application, local_conf, Settings, noname_enable, platform_ignore, platform_accept + Settings = ["ConfigFileRadio", "ConfigFileRadio", [], [], {}] + self.globals = Settings[4] # Will be replaced by quisk_settings.json + application = app + local_conf = self + noname_enable = [] + if sys.platform == 'win32': + platform_ignore = 'lin_' + platform_accept = 'win_' + else: + platform_accept = 'lin_' + platform_ignore = 'win_' + self.sections = [] + self.receiver_data = [] + self.StatePath = conf.settings_file_path + if not self.StatePath: + self.StatePath = os.path.join(app.QuiskFilesDir, "quisk_settings.json") + self.ReadState() + if AskMe == 'Same': + pass + elif Radio: + choices = Settings[2] + ["ConfigFileRadio"] + if Radio in choices: + if Settings[1] != Radio: + Settings[1] = Radio + self.settings_changed = True + else: + t = "There is no radio named %s. Radios are " % Radio + for choice in choices: + t = "%s%s, " % (t, choice) + t = t[0:-2] + '.' + dlg = wx.MessageDialog(application.main_frame, t, 'Specify Radio', wx.OK|wx.ICON_ERROR) + dlg.ShowModal() + dlg.Destroy() + sys.exit(0) + elif AskMe or Settings[0] == "Ask me": + choices = Settings[2] + ["ConfigFileRadio"] + dlg = wx.SingleChoiceDialog(None, "", "Start Quisk with this Radio", + choices, style=wx.DEFAULT_FRAME_STYLE|wx.OK|wx.CANCEL) + dlg.SetSizeHints(dlg.GetCharWidth() * 36, -1, -1, -1) + try: + n = choices.index(Settings[1]) # Set default to last used radio + except: + pass + else: + dlg.SetSelection(n) + ok = dlg.ShowModal() + if ok != wx.ID_OK: + sys.exit(0) + select = dlg.GetStringSelection() + dlg.Destroy() + if Settings[1] != select: + Settings[1] = select + self.settings_changed = True + else: + Settings[1] = Settings[0] + if Settings[1] == "ConfigFileRadio": + Settings[2].append("ConfigFileRadio") + Settings[3].append({}) + self.ParseConfig() + self.originalBandEdge = {} # save original BandEdge + self.originalBandEdge.update(conf.BandEdge) + self.UpdateGlobals() + def RequiredValues(self, radio_dict): + radio_type = radio_dict['hardware_file_type'] + # Fill in required values + if radio_type == "SdrIQ": + radio_dict["use_sdriq"] = '1' + if radio_dict['hardware_file_name'] == "sdriqpkg/quisk_hardware.py": + radio_dict['hardware_file_name'] = "quisk_hardware_sdriq.py" + else: + radio_dict["use_sdriq"] = '0' + if radio_type == "Hermes": + radio_dict["hermes_bias_adjust"] = "False" + if radio_type == 'SoapySDR': + radio_dict["use_soapy"] = '1' + self.InitSoapyNames(radio_dict) + if radio_dict.get("soapy_file_version", 0) < soapy_software_version: + text = "Your SoapySDR device parameters are out of date. Please go to the radio configuration screen and re-read the device parameters." + dlg = wx.MessageDialog(None, text, 'Please Re-Read Device', wx.OK|wx.ICON_INFORMATION) + dlg.ShowModal() + dlg.Destroy() + else: + radio_dict["use_soapy"] = '0' + if radio_type not in ("HiQSDR", "Hermes", "Red Pitaya", "Odyssey", "Odyssey2"): + radio_dict["use_rx_udp"] = '0' + if radio_type in ("Hermes", "Red Pitaya", "Odyssey2"): + if "Hermes_BandDict" not in radio_dict: + radio_dict["Hermes_BandDict"] = {} + if "Hermes_BandDictTx" not in radio_dict: + radio_dict["Hermes_BandDictTx"] = {} + def UpdateGlobals(self): + self.RadioName = Settings[1] + if self.RadioName == "ConfigFileRadio": + application.BandPlan = conf.BandPlan + elif "BandPlanColors" in Settings[4] and "BandPlan" in Settings[4]: + mode_dict = {"End":None} + for mode, color in Settings[4]["BandPlanColors"]: + mode_dict[mode] = color + plan = [] + for freq, mode in Settings[4]["BandPlan"]: + freq = int(float(freq) * 1E6 + 0.1) + try: + color = mode_dict[mode] + except: + print ("Missing color for mode", mode) + color = '#777777' + plan.append([freq, color]) + application.BandPlan = plan + else: + application.BandPlan = conf.BandPlan + if "MidiNoteDict" not in Settings[4]: + Settings[4]["MidiNoteDict"] = {} + self.settings_changed = True + self.MidiNoteDict = Settings[4]["MidiNoteDict"] + # Convert old to new format, October, 2021 + for txt_note in list(local_conf.MidiNoteDict): # txt_note is a string + int_note = int(txt_note, base=0) + if int_note < 128: + target = local_conf.MidiNoteDict[txt_note] + if len(target) > 3 and target[-3] == " " and target[-2] in "+-" and target[-1] in "0123456789": # Jog wheel + key = "0xB0%02X" % int_note + elif target in ("Vol", "STo", "Rit", "Ys", "Yz", "Zo", "Tune"): # Knob + key = "0xB0%02X" % int_note + else: # Button. Enter as the Note On message. + key = "0x90%02X" % int_note + local_conf.MidiNoteDict[key] = target + del local_conf.MidiNoteDict[txt_note] + self.settings_changed = True + def UpdateConf(self): # Called second to update the configuration for the selected radio + # Items in the radio_dict are generally strings. Convert these strings to Python integers, floats, + # etc. and write them to conf. + if Settings[1] == "ConfigFileRadio": + return + radio_dict = self.GetRadioDict() + # fill in conf from our configuration data; convert text items to Python objects + errors = '' + for k, v in list(radio_dict.items()): # radio_dict may change size during iteration + if k == 'favorites_file_path': # A null string is equivalent to "not entered" + if not v.strip(): + continue + if k in ('power_meter_local_calibrations', ): # present in configuration data but not in the config file + continue + if k[0:6] == 'soapy_': # present in configuration data but not in the config file + continue + if k[0:6] == 'Hware_': # contained in hardware file, not in configuration data nor config file + continue + try: + fmt = self.format4name[k] + except: + errors = errors + "Ignore obsolete parameter %s\n" % k + del radio_dict[k] + self.settings_changed = True + continue + k4 = k[0:4] + if k4 == platform_ignore: + continue + elif k4 == platform_accept: + k = k[4:] + fmt4 = fmt[0:4] + if fmt4 not in ('dict', 'list'): + i1 = v.find('#') + if i1 > 0: + v = v[0:i1] + try: + if fmt4 == 'text': # Note: JSON returns Unicode strings !!! + setattr(conf, k, v) + elif fmt4 == 'dict': + if isinstance(v, dict): + setattr(conf, k, v) + else: + raise ValueError() + elif fmt4 == 'list': + if isinstance(v, list): + setattr(conf, k, v) + else: + raise ValueError() + elif fmt4 == 'inte': + setattr(conf, k, int(v, base=0)) + elif fmt4 == 'numb': + setattr(conf, k, float(v)) + elif fmt4 == 'bool': + if v == "True": + setattr(conf, k, True) + else: + setattr(conf, k, False) + elif fmt4 == 'rfil': + pass + elif fmt4 == 'keyc': # key code + if v == "None": + x = None + else: + x = eval(v) + x = int(x) + if k == 'hot_key_ptt2' and not isinstance(x, int): + setattr(conf, k, wx.ACCEL_NORMAL) + else: + setattr(conf, k, x) + else: + print ("Unknown format for", k, fmt) + except: + del radio_dict[k] + self.settings_changed = True + errors = errors + "Failed to set %s to %s using format %s\n" % (k, v, fmt) + #traceback.print_exc() + if conf.color_scheme == 'B': + conf.__dict__.update(conf.color_scheme_B) + elif conf.color_scheme == 'C': + conf.__dict__.update(conf.color_scheme_C) + self.RequiredValues(radio_dict) # Why not update conf too??? This only updates the radio_dict. + if errors: + dlg = wx.MessageDialog(None, errors, + 'Update Settings', wx.OK|wx.ICON_ERROR) + ret = dlg.ShowModal() + dlg.Destroy() + def InitSoapyNames(self, radio_dict): # Set Soapy data items, but not the hardware available lists and ranges. + if radio_dict.get('soapy_getFullDuplex_rx', 0): + radio_dict["add_fdx_button"] = '1' + else: + radio_dict["add_fdx_button"] = '0' + name = 'soapy_gain_mode_rx' + if name not in radio_dict: + radio_dict[name] = 'total' + name = 'soapy_setAntenna_rx' + if name not in radio_dict: + radio_dict[name] = '' + name = 'soapy_gain_values_rx' + if name not in radio_dict: + radio_dict[name] = {} + name = 'soapy_gain_mode_tx' + if name not in radio_dict: + radio_dict[name] = 'total' + name = 'soapy_setAntenna_tx' + if name not in radio_dict: + radio_dict[name] = '' + name = 'soapy_gain_values_tx' + if name not in radio_dict: + radio_dict[name] = {} + def NormPath(self, path): # Convert between Unix and Window file paths + if sys.platform == 'win32': + path = path.replace('/', '\\') + else: + path = path.replace('\\', '/') + return path + def GetHardware(self): # Called third to open the hardware file + if Settings[1] == "ConfigFileRadio": + return False + path = self.GetRadioDict()["hardware_file_name"] + path = self.NormPath(path) + if not os.path.isfile(path): + dlg = wx.MessageDialog(None, + "Failure for hardware file %s!" % path, + 'Hardware File', wx.OK|wx.ICON_ERROR) + ret = dlg.ShowModal() + dlg.Destroy() + path = 'quisk_hardware_model.py' + dct = {} + dct.update(conf.__dict__) # make items from conf available + if "Hardware" in dct: + del dct["Hardware"] + if 'quisk_hardware' in dct: + del dct["quisk_hardware"] + exec(compile(open(path).read(), path, 'exec'), dct) + if "Hardware" in dct: + application.Hardware = dct['Hardware'](application, conf) + return True + return False + def Initialize(self): # Called fourth to fill in our ConfigFileRadio radio from conf + if Settings[1] == "ConfigFileRadio": + radio_dict = self.GetRadioDict("ConfigFileRadio") + typ = self.GuessType() + radio_dict['hardware_file_type'] = typ + all_data = [] + all_data = all_data + self.GetReceiverData(typ) + for name, sdata in self.sections: + all_data = all_data + sdata + for data_name, text, fmt, help_text, values in all_data: + data_name4 = data_name[0:4] + if data_name4 == platform_ignore: + continue + elif data_name4 == platform_accept: + conf_name = data_name[4:] + else: + conf_name = data_name + try: + if fmt in ("dict", "list"): + radio_dict[data_name] = getattr(conf, conf_name) + else: + radio_dict[data_name] = str(getattr(conf, conf_name)) + except: + if data_name == 'playback_rate': + pass + else: + print ('No config file value for', data_name) + def GetWidgets(self, app, hardware, conf, frame, gbs, vertBox): # Called fifth + if Settings[1] == "ConfigFileRadio": + return False + path = self.GetRadioDict().get("widgets_file_name", '') + path = self.NormPath(path) + if os.path.isfile(path): + dct = {} + dct.update(conf.__dict__) # make items from conf available + exec(compile(open(path).read(), path, 'exec'), dct) + if "BottomWidgets" in dct: + app.bottom_widgets = dct['BottomWidgets'](app, hardware, conf, frame, gbs, vertBox) + return True + def OnPageChanging(self, event): # Called when the top level page changes (not RadioNotebook pages) + event.Skip() + notebook = event.GetEventObject() + index = event.GetSelection() + page = notebook.GetPage(index) + if isinstance(page, RadioNotebook): + if not page.pages: + page.MakePages() + def AddPages(self, notebk, width): # Called sixth to add pages Help, Radios, all radio names + global win_width + win_width = width + self.notebk = notebk + self.radio_page = Radios(notebk) + notebk.AddPage(self.radio_page, "Radios") + self.radios_page_start = notebk.GetPageCount() + notebk.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.OnPageChanging, notebk) + for name in Settings[2]: + page = RadioNotebook(notebk, name) + if name == Settings[1]: + notebk.AddPage(page, "*%s*" % name) + else: + notebk.AddPage(page, name) + def GuessType(self): + udp = conf.use_rx_udp + if conf.use_sdriq: + return 'SdrIQ' + elif udp == 1: + return 'HiQSDR' + elif udp == 2: + return 'HiQSDR' + elif udp == 10: + return 'Hermes' + elif udp > 0: + return 'HiQSDR' + return 'SoftRock USB' + def AddRadio(self, radio_name, typ): + radio_dict = {} + radio_dict['hardware_file_type'] = typ + Settings[2].append(radio_name) + Settings[3].append(radio_dict) + for data_name, text, fmt, help_text, values in self.GetReceiverData(typ): + radio_dict[data_name] = values[0] + for name, data in self.sections: + for data_name, text, fmt, help_text, values in data: + radio_dict[data_name] = values[0] + # Change some default values in quisk_conf_defaults.py based on radio type + if typ in ("HiQSDR", "Hermes", "Red Pitaya", "Odyssey", "Odyssey2"): + radio_dict["add_fdx_button"] = '1' + page = RadioNotebook(self.notebk, radio_name) + self.notebk.AddPage(page, radio_name) + return True + def RenameRadio(self, old, new): + index = Settings[2].index(old) + n = self.radios_page_start + index + if old == Settings[1]: + self.notebk.SetPageText(n, "*%s*" % new) + else: + self.notebk.SetPageText(n, new) + Settings[2][index] = new + self.notebk.GetPage(n).NewName(new) + if old == "ConfigFileRadio": + for ctrl in noname_enable: + ctrl.Enable() + return True + def DeleteRadio(self, name): + index = Settings[2].index(name) + del Settings[2][index] + del Settings[3][index] + try: + n = self.radios_page_start + index + self.notebk.DeletePage(n) + except: + pass + return True + def GetRadioDict(self, radio_name=None): # None radio_name means the current radio + if radio_name: + index = Settings[2].index(radio_name) + else: # index of radio in use + index = Settings[2].index(Settings[1]) + return Settings[3][index] + #def GetItem(self, name, deflt=None, accept=None): # return item or default. accept can be "win_" or "lin_" + # dct = self.GetRadioDict() + # if accept: + # return dct.get(accept+name, deflt) + # return dct.get(name, deflt) + def GetSectionData(self, section_name): + for sname, data in self.sections: + if sname == section_name: + return data + return None + def GetReceiverData(self, receiver_name): + for rxname, data in self.receiver_data: + if rxname == receiver_name: + return data + return None + def GetReceiverDatum(self, receiver_name, item_name): + for rxname, data in self.receiver_data: + if rxname == receiver_name: + for data_name, text, fmt, help_text, values in data: + if item_name == data_name: + return values[0] + break + return '' + def GetReceiverItemTH(self, receiver_name, item_name): + for rxname, data in self.receiver_data: + if rxname == receiver_name: + for data_name, text, fmt, help_text, values in data: + if item_name == data_name: + return text, help_text + break + return '', '' + def ReceiverHasName(self, receiver_name, item_name): + for rxname, data in self.receiver_data: + if rxname == receiver_name: + for data_name, text, fmt, help_text, values in data: + if item_name == data_name: + return True + break + return False + def ReadState(self): + self.settings_changed = False + global Settings + try: + fp = open(self.StatePath, "r") + except: + return + try: + Settings = json.load(fp) + except: + traceback.print_exc() + fp.close() + try: # Do not save settings for radio ConfigFileRadio + index = Settings[2].index("ConfigFileRadio") + except ValueError: + pass + else: + del Settings[2][index] + del Settings[3][index] + for sdict in Settings[3]: + # Fixup for prior errors that save dictionaries as strings + for name in ("tx_level", "HiQSDR_BandDict", "Hermes_BandDict", "Hermes_BandDictTx"): + if name in sdict: + if not isinstance(sdict[name], dict): + print ("Bad dictionary for", name) + sdict[name] = {} + self.settings_changed = True + # Python None is saved as "null" + if "tx_level" in sdict: + if "null" in sdict["tx_level"]: + v = sdict["tx_level"]["null"] + sdict["tx_level"][None] = v + del sdict["tx_level"]["null"] + # Add global section if it is absent + length = len(Settings) + if length < 4: # Serious error + pass + elif length == 4: # Old file without global section + Settings.append({}) + self.settings_changed = True + else: # Settings[4] must be a dict + if not isinstance(Settings[4], dict): + Settings[4] = {} + self.settings_changed = True + self.globals = Settings[4] + def SaveState(self): + if not self.settings_changed: + return + try: + fp = open(self.StatePath, "w") + except: + traceback.print_exc() + return + json.dump(Settings, fp, indent=2) + fp.close() + self.settings_changed = False + def ParseConfig(self): + # ParseConfig() fills self.sections, self.receiver_data, and + # self.format4name with the items that Configuration understands. + # Dicts and lists are Python objects. All other items are text, not Python objects. + # + # Sections start with 16 #, section name + # self.sections is a list of [section_name, section_data] + # section_data is a list of [data_name, text, fmt, help_text, values] + + # Receiver sections start with 16 #, "Receivers ", receiver name, explain + # self.receiver_data is a list of [receiver_name, receiver_data] + # receiver_data is a list of [data_name, text, fmt, help_text, values] + + # Variable names start with ## variable_name variable_text, format + # The format is integer, number, text, boolean, integer choice, text choice, rfile + # Then some help text starting with "# " + # Then a list of possible value#explain with the default first + # Then a blank line to end. + self.format4name = {} + for name in name2format: + self.format4name[name] = name2format[name][0] + self.format4name['hardware_file_type'] = 'text' + self._ParserConf('quisk_conf_defaults.py') + # Read any user-defined radio types + for dirname in os.listdir('.'): + if not os.path.isdir(dirname) or dirname[-3:] != 'pkg': + continue + if dirname in ('freedvpkg', 'sdriqpkg', 'soapypkg'): + continue + filename = os.path.join(dirname, 'quisk_hardware.py') + if not os.path.isfile(filename): + continue + try: + self._ParserConf(filename) + except: + traceback.print_exc() + def _ParserConf(self, filename): + re_AeqB = re.compile("^#?(\w+)\s*=\s*([^#]+)#*(.*)") # item values "a = b" + section = None + data_name = None + multi_line = False + fp = open(filename, "r") + for line in fp: + line = line.strip() + if not line: + data_name = None + continue + if line[0:27] == '################ Receivers ': + section = 'Receivers' + args = line[27:].split(',', 1) + rxname = args[0].strip() + section_data = [] + self.receiver_data.append((rxname, section_data)) + elif line[0:17] == '################ ': + section = line[17:].strip() + if section in ('Colors', 'Obsolete'): + section = None + continue + rxname = None + section_data = [] + self.sections.append((section, section_data)) + if not section: + continue + if line[0:3] == '## ': # item_name item_text, format + args = line[3:].split(None, 1) + data_name = args[0] + args = args[1].split(',', 1) + dspl = args[0].strip() + fmt = args[1].strip() + value_list = [] + if data_name in self.format4name: + if self.format4name[data_name] != fmt: + print (filename, ": Inconsistent format for", data_name, self.format4name[data_name], fmt) + else: + self.format4name[data_name] = fmt + section_data.append([data_name, dspl, fmt, '', value_list]) + multi_line = False + if not data_name: + continue + if multi_line: + value += line + #print ("Multi", data_name, type(value), value) + count = self._multi_count(value) + if count == 0: + value = eval(value, conf.__dict__) + value_list.append(value) + #print ("Multi done", data_name, type(value), value) + multi_line = False + continue + mo = re_AeqB.match(line) + if mo: + if data_name != mo.group(1): + print (filename, ": Parse error for", data_name) + continue + value = mo.group(2).strip() + expln = mo.group(3).strip() + if value[0] in ('"', "'"): + value = value[1:-1] + elif value[0] == '{': # item is a dictionary + if self._multi_count(value) == 0: # dictionary is complete + value = eval(value, conf.__dict__) + #print ("Single", data_name, type(value), value) + else: + multi_line = True + #print ("Start multi", data_name, type(value), value) + continue + elif value[0] == '[': # item is a list + if self._multi_count(value) == 0: # list is complete + value = eval(value, conf.__dict__) + #print ("Single", data_name, type(value), value) + else: + multi_line = True + #print ("Start multi", data_name, type(value), value) + continue + if expln: + value_list.append("%s # %s" % (value, expln)) + else: + value_list.append(value) + elif line[0:2] == '# ': + section_data[-1][3] = section_data[-1][3] + line[2:] + ' ' + fp.close() + def _multi_count(self, value): + char_start = value[0] + if char_start == '{': + char_end = '}' + elif char_start == '[': + char_end = ']' + count = 0 + for ch in value: + if ch == char_start: + count += 1 + elif ch == char_end: + count -= 1 + return count + +class xxConfigHelp(wx.html.HtmlWindow): # The "Help with Radios" first-level page + """Create the help screen for the configuration tabs.""" + def __init__(self, parent): + wx.html.HtmlWindow.__init__(self, parent, -1, size=(win_width, 100)) + if "gtk2" in wx.PlatformInfo: + self.SetStandardFonts() + self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22]) + self.SetBackgroundColour(parent.bg_color) + # read in text from file help_conf.html in the directory of this module + self.LoadFile('help_conf.html') + +class QPowerMeterCalibration(wx.Frame): + """Create a window to enter the power output and corresponding ADC value AIN1/2""" + def __init__(self, parent, local_names): + self.parent = parent + self.local_names = local_names + self.table = [] # calibration table: list of [ADC code, power watts] + try: # may be missing in wxPython 2.x + wx.Frame.__init__(self, application.main_frame, -1, "Power Meter Calibration", + pos=(50, 100), style=wx.CAPTION|wx.FRAME_FLOAT_ON_PARENT) + except AttributeError: + wx.Frame.__init__(self, application.main_frame, -1, "Power Meter Calibration", + pos=(50, 100), style=wx.CAPTION) + panel = wx.Panel(self) + self.MakeControls(panel) + self.Show() + def MakeControls(self, panel): + charx = panel.GetCharWidth() + tab1 = charx * 5 + y = 20 + # line 1 + txt = wx.StaticText(panel, -1, 'Name for new calibration table', pos=(tab1, y)) + w, h = txt.GetSize().Get() + tab2 = tab1 + w + tab1 // 2 + self.cal_name = wx.TextCtrl(panel, -1, pos=(tab2, h), size=(charx * 16, h * 13 // 10)) + y += h * 3 + # line 2 + txt = wx.StaticText(panel, -1, 'Measured power level in watts', pos=(tab1, y)) + self.cal_power = wx.TextCtrl(panel, -1, pos=(tab2, y), size=(charx * 16, h * 13 // 10)) + x = tab2 + charx * 20 + add = QuiskPushbutton(panel, self.OnBtnAdd, "Add to Table") + add.SetPosition((x, y - h * 3 // 10)) + add.SetColorGray() + ww, hh = add.GetSize().Get() + width = x + ww + tab1 + y += h * 3 + # line 3 + sv = QuiskPushbutton(panel, self.OnBtnSave, "Save") + sv.SetColorGray() + cn = QuiskPushbutton(panel, self.OnBtnCancel, "Cancel") + cn.SetColorGray() + w, h = cn.GetSize().Get() + sv.SetPosition((width // 4, y)) + cn.SetPosition((width - width // 4 - w, y)) + y += h * 12 // 10 + # help text at bottom + wx.StaticText(panel, -1, '1. Attach a 50 ohm load and power meter to the antenna connector.', pos=(tab1, y)) + w, h = txt.GetSize().Get() + h = h * 12 // 10 + y += h + wx.StaticText(panel, -1, '2. Use the Spot button to transmit at a very low power.', pos=(tab1, y)) + y += h + wx.StaticText(panel, -1, '3. Enter the measured power in the box above and press "Add to Table".', pos=(tab1, y)) + y += h + wx.StaticText(panel, -1, '4. Increase the power a small amount and repeat step 3.', pos=(tab1, y)) + y += h + wx.StaticText(panel, -1, '5. Increase power again and repeat step 3.', pos=(tab1, y)) + y += h + wx.StaticText(panel, -1, '6. Keep adding measurements to the table until you reach full power.', pos=(tab1, y)) + y += h + wx.StaticText(panel, -1, '7. Ten or twelve measurements should be enough. Then press "Save".', pos=(tab1, y)) + y += h + wx.StaticText(panel, -1, 'To delete a table, save a table with zero measurements.', pos=(tab1, y)) + y += h * 2 + self.SetClientSize(wx.Size(width, y)) + def OnBtnCancel(self, event=None): + self.parent.ChangePMcalFinished(None, None) + self.Destroy() + def OnBtnSave(self, event): + name = self.cal_name.GetValue().strip() + if not name: + dlg = wx.MessageDialog(self, + 'Please enter a name for the new calibration table.', + 'Missing Name', wx.OK|wx.ICON_ERROR) + dlg.ShowModal() + dlg.Destroy() + elif name in conf.power_meter_std_calibrations: # known calibration names from the config file + dlg = wx.MessageDialog(self, + 'That name is reserved. Please enter a different name.', + 'Reserved Name', wx.OK|wx.ICON_ERROR) + dlg.ShowModal() + dlg.Destroy() + elif name in self.local_names: + if self.table: + dlg = wx.MessageDialog(self, + 'That name exists. Replace the existing table?', + 'Replace Table', wx.OK|wx.CANCEL|wx.ICON_EXCLAMATION) + ret = dlg.ShowModal() + dlg.Destroy() + if ret == wx.ID_OK: + self.parent.ChangePMcalFinished(name, self.table) + self.Destroy() + else: + dlg = wx.MessageDialog(self, + 'That name exists but the table is empty. Delete the existing table?.', + 'Delete Table', wx.OK|wx.CANCEL|wx.ICON_EXCLAMATION) + ret = dlg.ShowModal() + dlg.Destroy() + if ret == wx.ID_OK: + self.parent.ChangePMcalFinished(name, None) + self.Destroy() + else: + self.parent.ChangePMcalFinished(name, self.table) + self.Destroy() + def OnBtnAdd(self, event): + power = self.cal_power.GetValue().strip() + self.cal_power.Clear() + try: + power = float(power) + except: + dlg = wx.MessageDialog(self, 'Missing or bad measured power.', 'Error in Power', wx.OK|wx.ICON_ERROR) + dlg.ShowModal() + dlg.Destroy() + else: + ## Convert measured voltage to power + #power *= 6.388 + #power = power**2 / 50.0 + fwd = application.Hardware.hermes_fwd_power + rev = application.Hardware.hermes_rev_power + if fwd >= rev: + self.table.append([fwd, power]) # Item must use lists; sort() will fail with mixed lists and tuples + else: + self.table.append([rev, power]) + +## Note: The amplitude/phase adjustments have ideas provided by Andrew Nilsson, VK6JBL. +## October 2020: changed to make RxTx frequency and VFO two independent variables. +class QAdjustPhase(wx.Frame): + """Create a window with amplitude and phase adjustment controls""" + f_ampl = "Amplitude adjustment %.6f" + f_phase = "Phase adjustment degrees %.6f" + def __init__(self, parent, width, rx_tx): + self.width = width + self.rx_tx = rx_tx # Must be "rx" or "tx" + if rx_tx == 'tx': + self.is_tx = 1 + t = "Adjust Sound Card Transmit Amplitude and Phase" + else: + self.is_tx = 0 + t = "Adjust Sound Card Receive Amplitude and Phase" + wx.Frame.__init__(self, application.main_frame, -1, t, pos=(50, 100), style=wx.CAPTION) + self.panel = wx.Panel(self) + self.MakeControls() + self.Redraw() + self.Show() + def MakeControls(self): # Make controls for phase/amplitude adjustment + panel = self.panel + width = self.width + self.old_amplitude, self.old_phase = application.GetAmplPhase(self.rx_tx) + self.new_amplitude, self.new_phase = self.old_amplitude, self.old_phase + sl_max = width * 4 // 10 # maximum +/- value for slider + self.ampl_scale = float(conf.rx_max_amplitude_correct) / sl_max + self.phase_scale = float(conf.rx_max_phase_correct) / sl_max + font = wx.Font(conf.default_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL, + wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + self.SetFont(font) + charx = self.GetCharWidth() + self.tab1 = tab1 = charx + chary = self.GetCharHeight() + deltay = chary * 12 // 10 + y = chary * 3 // 10 + self.controls = [] + txt = wx.StaticText(panel, -1, "No data", pos=(tab1, y)) + self.controls.append(txt) + y += txt.GetSize().GetHeight() + #y += deltay * 14 // 10 + self.t_ampl = wx.StaticText(panel, -1, self.f_ampl % self.old_amplitude, pos=(tab1, y)) + self.controls.append(self.t_ampl) + y += deltay + fine = wx.StaticText(panel, -1, 'Fine', pos=(tab1, y)) + self.controls.append(fine) + coarse = wx.StaticText(panel, -1, 'Coarse', pos=(tab1, y + deltay)) + self.controls.append(coarse) + tab2 = tab1 + coarse.GetSize().GetWidth() + charx + sliderX = width - tab2 - charx + self.ampl1 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1)) + self.controls.append(self.ampl1) + y += deltay + self.ampl2 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1)) + self.controls.append(self.ampl2) + y += deltay * 14 // 10 + self.PosAmpl(self.old_amplitude) + self.t_phase = wx.StaticText(panel, -1, self.f_phase % self.old_phase, pos=(tab1, y)) + self.controls.append(self.t_phase) + y += deltay + fine = wx.StaticText(panel, -1, 'Fine', pos=(tab1, y)) + self.controls.append(fine) + coarse = wx.StaticText(panel, -1, 'Coarse', pos=(tab1, y + deltay)) + self.controls.append(coarse) + self.phase1 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1)) + self.controls.append(self.phase1) + y += deltay + self.phase2 = wx.Slider(panel, -1, 0, -sl_max, sl_max, pos=(tab2, y), size=(sliderX, -1)) + self.controls.append(self.phase2) + y += deltay + sv = QuiskPushbutton(panel, self.OnBtnSave, 'Save') + self.controls.append(sv) + dv = QuiskPushbutton(panel, self.OnBtnDestroyVFO, 'Destroy VFO') + self.controls.append(dv) + ds = QuiskPushbutton(panel, self.OnBtnDestroyALL, 'Destroy ALL') + self.controls.append(ds) + cn = QuiskPushbutton(panel, self.OnBtnFinished, 'Finished') + self.controls.append(cn) + hl = QuiskPushbutton(panel, self.OnBtnHelp, 'Help') + self.controls.append(hl) + w, h = ds.GetSize().Get() + sv.SetSize((w, h)) + cn.SetSize((w, h)) + hl.SetSize((w, h)) + y += h * 5 // 10 + x = (width - w * 5) // 6 + sv.SetPosition((x, y)) + dv.SetPosition((x*2 + w, y)) + ds.SetPosition((x*3 + w*2, y)) + cn.SetPosition((x*4 + w*3, y)) + hl.SetPosition((x*5 + w*4, y)) + sv.SetBackgroundColour('light blue') + dv.SetBackgroundColour('light blue') + ds.SetBackgroundColour('light blue') + cn.SetBackgroundColour('light blue') + hl.SetBackgroundColour('light blue') + y += h + y += h * 4 // 10 + self.PosPhase(self.old_phase) + self.SetClientSize(wx.Size(width, y)) + self.ampl1.Bind(wx.EVT_SCROLL, self.OnChange) + self.ampl2.Bind(wx.EVT_SCROLL, self.OnAmpl2) + self.phase1.Bind(wx.EVT_SCROLL, self.OnChange) + self.phase2.Bind(wx.EVT_SCROLL, self.OnPhase2) + def Redraw(self): + # Print available data points + self.band = application.lastBand + data = application.bandAmplPhase.get(self.band, {}) + data = data.get(self.rx_tx, []) + if data: + t = "Band %s VFO and frequency list\n" % self.band + for vfo, items in data: + t += "%10d:" % vfo + for freq, ampl, phase in items: + t += " %7d" % freq + t += "\n" + else: + t = "Band %s: No data." % self.band + x, y = self.controls[0].GetPosition() + height = self.controls[0].GetSize().GetHeight() + self.controls[0].Destroy() + txt = wx.StaticText(self.panel, -1, t, pos=(self.tab1, y)) + self.controls[0] = txt + delta = txt.GetSize().GetHeight() - height + for ctrl in self.controls[1:]: + x, y = ctrl.GetPosition() + y += delta + ctrl.Move(x, y) + h = self.GetClientSize().GetHeight() + self.SetClientSize(wx.Size(self.width, h + delta)) + def PosAmpl(self, ampl): # set pos1, pos2 for amplitude + pos2 = round(ampl / self.ampl_scale) + remain = ampl - pos2 * self.ampl_scale + pos1 = round(remain / self.ampl_scale * 50.0) + self.ampl1.SetValue(pos1) + self.ampl2.SetValue(pos2) + def PosPhase(self, phase): # set pos1, pos2 for phase + pos2 = round(phase / self.phase_scale) + remain = phase - pos2 * self.phase_scale + pos1 = round(remain / self.phase_scale * 50.0) + self.phase1.SetValue(pos1) + self.phase2.SetValue(pos2) + def OnChange(self, event): + ampl = self.ampl_scale * self.ampl1.GetValue() / 50.0 + self.ampl_scale * self.ampl2.GetValue() + if abs(ampl) < self.ampl_scale * 3.0 / 50.0: + ampl = 0.0 + self.t_ampl.SetLabel(self.f_ampl % ampl) + phase = self.phase_scale * self.phase1.GetValue() / 50.0 + self.phase_scale * self.phase2.GetValue() + if abs(phase) < self.phase_scale * 3.0 / 50.0: + phase = 0.0 + self.t_phase.SetLabel(self.f_phase % phase) + QS.set_ampl_phase(ampl, phase, self.is_tx) + self.new_amplitude, self.new_phase = ampl, phase + def OnAmpl2(self, event): # re-center the fine slider when the coarse slider is adjusted + ampl = self.ampl_scale * self.ampl1.GetValue() / 50.0 + self.ampl_scale * self.ampl2.GetValue() + self.PosAmpl(ampl) + self.OnChange(event) + def OnPhase2(self, event): # re-center the fine slider when the coarse slider is adjusted + phase = self.phase_scale * self.phase1.GetValue() / 50.0 + self.phase_scale * self.phase2.GetValue() + self.PosPhase(phase) + self.OnChange(event) + def OnBtnDestroyVFO(self, event): # Remove entry with the application VFO + data = application.bandAmplPhase.get(self.band, {}) + data = data.get(self.rx_tx, []) + for i in range(len(data)): + if data[i][0] == application.VFO: + del data[i] + break + self.Redraw() + def OnBtnSave(self, event): + data = application.bandAmplPhase + if self.band not in data: + data[self.band] = {} + data = data[self.band] + if self.rx_tx not in data: + data[self.rx_tx] = [] + data = data[self.rx_tx] + for vfo, items in data: # All items must be lists, not tuples + if vfo == application.VFO: + for i in range(len(items)): + item = items[i] + if item[0] == application.rxFreq: + item[1] = self.new_amplitude + item[2] = self.new_phase + break + else: + items.append([application.rxFreq, self.new_amplitude, self.new_phase]) + items.sort() + break + else: + data.append([application.VFO, [[application.rxFreq, self.new_amplitude, self.new_phase]]]) + data.sort() + self.Redraw() + def OnBtnDestroyALL(self, event): + dlg = wx.MessageDialog(self, "This will destroy all data for band %s!" % self.band, + "Destroy Data", style = wx.YES|wx.NO) + if dlg.ShowModal() == wx.ID_YES: + if self.band in application.bandAmplPhase: + del application.bandAmplPhase[self.band] + self.Redraw() + def OnBtnFinished(self, event=None): + ampl, phase = application.GetAmplPhase(self.rx_tx) + QS.set_ampl_phase(ampl, phase, self.is_tx) + application.w_phase = None + self.Destroy() + def OnBtnHelp(self, event=None): + dlg = wx.MessageDialog(self, +'The "VFO" is the frequency at the center of the graph screen. The Rx or Tx frequency is the offset from the VFO. \ +Adjust the VFO and the frequency as desired. Then adjust the sliders to minimize the image. Press "Save" when satisfied. \ +To adjust the VFO, use the band Up/Down buttons, or right click the graph at the desired VFO. \ +Adjustments must be made for both receive and transmit on each band. \ +The maximum slider adjustment range can be changed on the radio Hardware screen. \ +The other buttons will delete the data for the current VFO, or for the whole band. \ +For more information, press the main "Help" button, \ +then "Documentation", then "SoftRock".' + , "Adjustment Help", style=wx.OK) + dlg.ShowModal() + +class ListEditDialog(wx.Dialog): # Display a dialog with a List-Edit control, plus Ok/Cancel + def __init__(self, parent, title, choice, choices, width): + wx.Dialog.__init__(self, parent, title=title, style=wx.CAPTION|wx.CLOSE_BOX) + cancel = wx.Button(self, wx.ID_CANCEL, "Cancel") + bsize = cancel.GetSize() + margin = bsize.height + self.combo = wx.ComboBox(self, -1, choice, pos=(margin, margin), size=(width - margin * 2, -1), choices=choices, style=wx.CB_DROPDOWN) + y = margin + self.combo.GetSize().height + margin + x = width - margin * 2 - bsize.width * 2 + x = x // 3 + ok = wx.Button(self, wx.ID_OK, "OK", pos=(margin + x, y)) + cancel.SetPosition((width - margin - x - bsize.width, y)) + self.SetClientSize(wx.Size(width, y + bsize.height * 14 // 10)) + def GetValue(self): + return self.combo.GetValue() + +class RadioNotebook(wx.Notebook): # The second-level notebook for each radio name + def __init__(self, parent, radio_name): + wx.Notebook.__init__(self, parent) + font = wx.Font(conf.config_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL, + wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + self.SetFont(font) + self.SetBackgroundColour(parent.bg_color) + self.radio_name = radio_name + self.pages = [] + def MakePages(self): + radio_name = self.radio_name + radio_dict = local_conf.GetRadioDict(radio_name) + radio_type = radio_dict['hardware_file_type'] + if radio_type == 'SoapySDR': + page = RadioHardwareSoapySDR(self, radio_name) + else: + page = RadioHardware(self, radio_name) + self.AddPage(page, "Hardware") + self.pages.append(page) + page = RadioSound(self, radio_name) + self.AddPage(page, "Sound") + self.pages.append(page) + for section, names in local_conf.sections: + if section in ('Sound', 'Bands', 'Filters'): # There is a special page for these sections + continue + page = RadioSection(self, radio_name, section, names) + self.AddPage(page, section) + self.pages.append(page) + page = RadioBands(self, radio_name) + self.AddPage(page, "Bands") + self.pages.append(page) + #if "use_rx_udp" in radio_dict and radio_dict["use_rx_udp"] == '10': + # page = RadioFilters(self, radio_name) + # self.AddPage(page, "Filters") + # self.pages.append(page) + def NewName(self, new_name): + self.radio_name = new_name + for page in self.pages: + page.radio_name = new_name + +class ChoiceCombo(wx.Choice): + text_for_blank = "-blank-" + def __init__(self, parent, value, choices): + wx.Choice.__init__(self, parent, choices=choices) + self.choices = choices[:] + self.handler = None + try: + index = self.choices.index(value) + except: + index = 0 + self.Bind(wx.EVT_CHOICE, self.OnChoice) + self._ChangeItems(index) + def _ChangeItems(self, index): + length = len(self.choices) + if length <= 0: + self.Enable(False) + elif length == 1: + wx.Choice.SetSelection(self, 0) + self.Enable(False) + else: + wx.Choice.SetSelection(self, index) + self.Enable(True) + self._ReplaceBlank() + self.GetValue() + def _ReplaceBlank(self): + try: + n = self.choices.index('') + except: + pass + else: + self.SetString(n, self.text_for_blank) + def SetItems(self, lst): + wx.Choice.SetItems(self, lst) + self.choices = lst[:] + self._ChangeItems(0) + def SetSelection(self, n): + if 0 <= n < len(self.choices): + wx.Choice.SetSelection(self, n) + self.GetValue() + def SetText(self, text): + try: + n = self.choices.index(text) + except: + print("Failed to set choice list to", text) + else: + wx.Choice.SetSelection(self, n) + self.GetValue() + def GetValue(self): + n = self.GetSelection() + if n == wx.NOT_FOUND: + self.value = '' + else: + self.value = self.GetString(n) + if self.value == self.text_for_blank: + self.value = '' + return self.value + def OnChoice(self, event): + event.Skip() + old = self.value + self.GetValue() + if self.value != old: + if self.handler: + self.handler(self) + +class ComboCtrl(wxcombo.ComboCtrl): + def __init__(self, parent, value, choices, no_edit=False): + self.value = value + self.choices = choices[:] + self.handler = None + self.dirty = False + try: + self.bgColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_LISTBOX) + except: + self.bgColor = wx.Colour('#f6f5f4') + if no_edit: + wxcombo.ComboCtrl.__init__(self, parent, -1, style=wx.CB_READONLY) + else: + wxcombo.ComboCtrl.__init__(self, parent, -1, style=wx.TE_PROCESS_ENTER) + self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) + self.Bind(wx.EVT_TEXT, self.OnText) + self.Bind(wx.EVT_TEXT_ENTER, self.OnTextEnter) + try: + self.height = parent.quisk_height + except: + self.chary = self.GetCharHeight() + self.height = self.chary * 14 // 10 + self.SetBackgroundColour(self.bgColor) + self.Refresh() + try: + font = parent.font + except: + font = self.GetFont() + self.ctrl = ListBoxComboPopup(choices, font) + self.SetPopupControl(self.ctrl) + self.SetText(value) + self.SetSizes() + self.Bind(wx.EVT_COMBOBOX_CLOSEUP, self.OnCloseup) + def SetItems(self, lst): + self.ctrl.SetItems(lst) + self.choices = lst[:] + self.SetSizes() + def SetSizes(self): + charx = self.GetCharWidth() + wm = charx + w, h = self.GetTextExtent(self.value) + if wm < w: + wm = w + for ch in self.choices: + w, h = self.GetTextExtent(ch) + if wm < w: + wm = w + wm += charx * 5 + self.SetSizeHints(wm, self.height, 9999, self.height) + def SetSelection(self, n): # Set text to item in list box. Name conflict with wxcombo.ComboCtrl. + try: + text = self.choices[n] + except IndexError: + self.SetText('') + self.value = '' + else: + self.ctrl.SetSelection(n) + self.SetText(text) + self.value = text + def OnText(self, event): + self.dirty = True + self.SetBackgroundColour('#e0e0ff') + self.Refresh() + def OnTextEnter(self, event=None): + self.SetBackgroundColour(self.bgColor) + self.Refresh() + self.dirty = False + if event: + event.Skip() + new = self.GetValue() + if self.value != new: + self.value = new + if self.handler: + self.handler(self) + def OnKillFocus(self, event): + event.Skip() + if self.dirty: + self.OnTextEnter(None) + def OnListbox(self): + self.Dismiss() + self.OnTextEnter() + def OnButtonClick(self): + wxcombo.ComboCtrl.OnButtonClick(self) + wxcombo.ComboCtrl.SetSelection(self, 0, 0) + wxcombo.ComboCtrl.SetInsertionPointEnd(self) + def OnCloseup(self, event): + event.Skip() + wxcombo.ComboCtrl.SetSelection(self, 0, 0) + wxcombo.ComboCtrl.SetInsertionPointEnd(self) + +class ListBoxComboPopup(wxcombo.ComboPopup): + text_for_blank = "-blank-" + def __init__(self, choices, font): + wxcombo.ComboPopup.__init__(self) + self.choices = choices + self.font = font + self.lbox = None + self.index = None + def Create(self, parent): + self.lbox = wx.ListBox(parent, choices=self.choices, style=wx.LB_SINGLE|wx.LB_NEEDED_SB) + self.lbox.SetFont(self.font) + self.lbox.Bind(wx.EVT_MOTION, self.OnMotion) + self.lbox.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self._ReplaceBlank() + return True + def SetItems(self, lst): + self.choices = lst[:] + self.lbox.Set(self.choices) + self._ReplaceBlank() + def _ReplaceBlank(self): + try: + n = self.choices.index('') + except: + pass + else: + self.lbox.SetString(n, self.text_for_blank) + def SetSelection(self, n): + self.lbox.SetSelection(n) + def GetStringValue(self): + try: + text = self.choices[self.lbox.GetSelection()] + except IndexError: + text = '' + else: + if text == self.text_for_blank: + text = '' + return text + def GetAdjustedSize(self, minWidth, prefHeight, maxHeight): + chary = self.lbox.GetCharHeight() + height = chary * len(self.choices) * 15 // 10 + chary + if height > prefHeight: + height = prefHeight + return (minWidth, height) + def OnLeftDown(self, event): + event.Skip() + self.index = self.lbox.GetSelection() # index of selected item + if wxVersion in ('2', '3'): + self.GetCombo().OnListbox() + else: + self.GetComboCtrl().OnListbox() + def OnMotion(self, event): + event.Skip() + item = self.lbox.HitTest(event.GetPosition()) + if item >= 0: + self.lbox.SetSelection(item) + def GetControl(self): + return self.lbox + +class QuiskTextCtrl(wx.TextCtrl): + def __init__(self, parent, text, style): + wx.TextCtrl.__init__(self, parent, -1, text, style=style) + self.dirty = False + self.bgColor = wx.Colour('#f6f5f4') + #self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) + if not (style & wx.TE_READONLY): + self.Bind(wx.EVT_TEXT, self.OnText) + self.Bind(wx.EVT_TEXT_ENTER, self.OnTextEnter) + def SetValue(self, value): + wx.TextCtrl.SetValue(self, value) + self.SetBackgroundColour(self.bgColor) + self.Refresh() + self.dirty = False + def OnText(self, event): + event.Skip() + self.dirty = True + self.SetBackgroundColour('#e0e0ff') + self.Refresh() + def OnTextEnter(self, event=None): + if event: + event.Skip() + self.SetBackgroundColour(self.bgColor) + self.Refresh() + self.dirty = False + #def OnKillFocus(self, event): + # event.Skip() + # if self.dirty: + # self.OnTextEnter(None) + +class QuiskControl(wx.Control): + def __init__(self, parent, text, height, pos=wx.DefaultPosition, style=0): + wx.Control.__init__(self, parent, -1, pos=pos, style=style) + self.text = text + self.handler = None + self.value = '' + if wxVersion in ('2', '3'): + self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) + else: + self.SetBackgroundStyle(wx.BG_STYLE_PAINT) + from quisk_widgets import button_font + self.SetFont(button_font) + w, h = self.GetTextExtent(text) + self.SetSizeHints(w + height * 3, height, 999, height) + self.bg_brush = wx.Brush('#dcdcdc') + self.black_brush = wx.Brush(wx.Colour(0x000000)) + self.text_color = wx.Colour(0x000000) + self.black_pen = wx.Pen(self.text_color) + self.no_pen = wx.Pen(self.text_color, style=wx.PENSTYLE_TRANSPARENT) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.MakeMenu() + def OnPaint(self, event): + dc = wx.AutoBufferedPaintDC(self) + dc.SetBackground(self.bg_brush) + dc.SetTextForeground(self.text_color) + dc.Clear() + self._Glyph1(dc) + def _Glyph1(self, dc): + win_w, win_h = self.GetClientSize() + x = win_w - win_h + l = win_h * 20 // 100 + if l < 3: + l = 3 + y = win_h // 2 - 2 + pen = wx.Pen('#555555', 1) + dc.SetPen(pen) + points = [(0, 0), (l, l), (l + 1, l), (l * 2 + 2, -1)] + dc.DrawLines(points, x, y) + pen = wx.Pen('#888888', 1) + dc.SetPen(pen) + points = [(1, 0), (l, l - 1), (l + 1, l - 1), (l * 2 + 1, -1)] + dc.DrawLines(points, x, y) + w, h = dc.GetTextExtent(self.text) + x = (win_w - win_h - w) // 2 + if x < 0: + x = 0 + y = (win_h - h) // 2 + if y < 0: + y = 0 + dc.DrawText(self.text, x, y) + def _Glyph2(self, dc): # Not used + dc.SetBrush(self.black_brush) + win_w, win_h = self.GetClientSize() + spacing = 2 + rect_h = (win_h - spacing * 4) // 3 + if rect_h % 2 == 0: # Make rect_h an odd number + rect_h -= 1 + spacing = (win_h - rect_h * 3) // 4 + width = rect_h * 25 // 10 + x0 = win_w - (rect_h + 2 + width + 3) + y0 = (win_h - rect_h * 3 - spacing * 2) // 2 + x = (x0 - dc.GetCharWidth()) // 2 + y = (win_h - dc.GetCharHeight()) // 2 + dc.DrawText(u"\u2BA9", x, y) + for y in range(3): + dc.SetPen(self.no_pen) + dc.DrawRectangle(x0, y0, rect_h, rect_h) + x = x0 + rect_h + 2 + y = y0 + rect_h // 2 + dc.SetPen(self.black_pen) + dc.DrawLine(x, y, x + width, y) + y0 += rect_h + spacing + def MakeMenu(self): + self.menu = wx.Menu() + def OnLeftDown(self, event): + pos = wx.Point(0, 0) + self.PopupMenu(self.menu, pos) + +class MidiButton(QuiskControl): + def __init__(self, parent, text, height, pos=wx.DefaultPosition, style=0): + QuiskControl.__init__(self, parent, text, height, pos, style) + def MakeMenu(self): + main_names = [] + bands = wx.Menu() + filters = wx.Menu() + modes = wx.Menu() + screens = wx.Menu() + for idName in application.idName2Button: + if not idName: + item = None + elif idName in conf.BandList or idName in ("Audio", "Time"): + item = bands.Append(-1, idName) + elif idName[0:7] == 'Filter ': + item = filters.Append(-1, idName) + elif idName in ("CW U/L", "CWL", "CWU", "SSB U/L", "LSB", "USB", "AM", "FM", "DGT", + "DGT-U", "DGT-L", "DGT-FM", "DGT-IQ", "FDV", "FDV-U", "IMD", ): + item = modes.Append(-1, idName) + elif idName in ("Graph", "GraphP1", "GraphP2", "WFall", "Scope", "Config", "Audio FFT", "Bscope", "RX Filter", "Help"): + item = screens.Append(-1, idName) + else: + item = None + main_names.append(idName) + if item: + self.Bind(wx.EVT_MENU, self.OnMenu, item) + main_names.sort() + self.menu = wx.Menu() + main1 = wx.Menu() + main2 = wx.Menu() + for name in main_names: + if name[0].upper() in "ABCDEFGHIJKLMN": + item = main1.Append(-1, name) + else: + item = main2.Append(-1, name) + self.Bind(wx.EVT_MENU, self.OnMenu, item) + self.menu.AppendSubMenu(main1, "Button A-N") + self.menu.AppendSubMenu(main2, "Button O-Z") + item = self.menu.AppendSubMenu(bands, "Bands") + if bands.GetMenuItemCount() <= 0: + self.menu.Enable(item.GetId(), False) + item = self.menu.AppendSubMenu(filters, "Filters") + if filters.GetMenuItemCount() <= 0: + self.menu.Enable(item.GetId(), False) + item = self.menu.AppendSubMenu(modes, "Modes") + if modes.GetMenuItemCount() <= 0: + self.menu.Enable(item.GetId(), False) + item = self.menu.AppendSubMenu(screens, "Screens") + if screens.GetMenuItemCount() <= 0: + self.menu.Enable(item.GetId(), False) + def OnMenu(self, event): + self.value = self.menu.GetLabel(event.GetId()) + if self.handler: + self.handler(self) + +class MidiKnob(QuiskControl): + def __init__(self, parent, text, height, pos=wx.DefaultPosition, style=0): + QuiskControl.__init__(self, parent, text, height, pos, style) + def MakeMenu(self): + self.menu = wx.Menu() + for name in application.midiControls: + ctrl, func = application.midiControls[name] + if not ctrl or not func: + continue + item = self.menu.Append(-1, name) + self.Bind(wx.EVT_MENU, self.OnMenu, item) + def OnMenu(self, event): + self.value = self.menu.GetLabel(event.GetId()) + if self.handler: + self.handler(self) + +class MidiJogWheel(QuiskControl): + def __init__(self, parent, text, height, pos=wx.DefaultPosition, style=0): + self.jog_direction = "+" + self.jog_speed = "5" + self.MakeText() + QuiskControl.__init__(self, parent, self.text, height, pos, style) + def MakeText(self): + self.text = "Jog Wheel %s%s" % (self.jog_direction, self.jog_speed) + def MakeMenu(self): + self.menu = wx.Menu() + self.direc_menu = wx.Menu() + item = self.direc_menu.AppendRadioItem(-1, "Move +") + self.Bind(wx.EVT_MENU, self.OnDirecMenu, item) + item = self.direc_menu.AppendRadioItem(-1, "Move -") + self.Bind(wx.EVT_MENU, self.OnDirecMenu, item) + self.menu.AppendSubMenu(self.direc_menu, "Direction") + self.speed_menu = wx.Menu() + for i in range(10): + item = self.speed_menu.AppendRadioItem(-1, "Speed %d" % i) + self.Bind(wx.EVT_MENU, self.OnSpeedMenu, item) + if i == 5: + item.Check() + self.menu.AppendSubMenu(self.speed_menu, "Speed") + controls = wx.Menu() + for name in application.midiControls: + ctrl, func = application.midiControls[name] + if not ctrl or not func: + continue + item = controls.Append(-1, name) + self.Bind(wx.EVT_MENU, self.OnMenu, item) + self.menu.AppendSubMenu(controls, "Name") + def OnMenu(self, event): + value = self.menu.GetLabel(event.GetId()) + self.value = "%s %s%s" % (value, self.jog_direction, self.jog_speed) + if self.handler: + self.handler(self) + def OnDirecMenu(self, event): + value = self.menu.GetLabel(event.GetId()) + self.jog_direction = value[-1] + self.MakeText() + self.Refresh() + if self.handler: + self.handler(self, "%s%s" % (self.jog_direction, self.jog_speed)) + def OnSpeedMenu(self, event): + value = self.menu.GetLabel(event.GetId()) + self.jog_speed = value[-1] + self.MakeText() + self.Refresh() + if self.handler: + self.handler(self, "%s%s" % (self.jog_direction, self.jog_speed)) + def ChangeSelection(self, text): + self.jog_direction = text[0] + self.jog_speed = text[1] + self.MakeText() + self.Refresh() + if self.jog_direction == '+': + self.direc_menu.FindItemByPosition(0).Check() + else: + self.direc_menu.FindItemByPosition(1).Check() + index = int(self.jog_speed) + self.speed_menu.FindItemByPosition(index).Check() + +class ControlMixin: + def __init__(self, parent): + self.font = wx.Font(conf.config_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL, + wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + self.SetFont(self.font) + self.row = 1 + self.charx = self.GetCharWidth() + self.chary = self.GetCharHeight() + self.quisk_height = self.chary * 14 // 10 + # GBS + self.gbs = wx.GridBagSizer(2, 2) + self.gbs.SetEmptyCellSize((self.charx, self.charx)) + self.SetSizer(self.gbs) + self.gbs.Add((self.charx, self.charx), (0, 0)) + def MarkCols(self): + for col in range(1, self.num_cols): + c = wx.StaticText(self, -1, str(col % 10)) + self.gbs.Add(c, (self.row, col)) + self.row += 1 + def NextRow(self, row=None): + if row is None: + self.row += 1 + else: + self.row = row + def AddHelpButton(self, col, text, help_text, border=1): + hbtn = QuiskPushbutton(self, self._BTnHelp, "..") + hbtn.SetColorGray() + hbtn.quisk_help_text = help_text + hbtn.quisk_caption = text + h = self.quisk_height + 2 + hbtn.SetSizeHints(h, h, h, h) + self.gbs.Add(hbtn, (self.row, col), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx*border) + def AddTextL(self, col, text, span=None): + c = wx.StaticText(self, -1, text) + if col < 0: + pass + elif span is None: + self.gbs.Add(c, (self.row, col), flag=wx.ALIGN_CENTER_VERTICAL) + else: + self.gbs.Add(c, (self.row, col), span=(1, span), flag=wx.ALIGN_CENTER_VERTICAL) + return c + def AddTextC(self, col, text, span=None, flag=wx.ALIGN_CENTER): + c = wx.StaticText(self, -1, text) + if col < 0: + pass + elif span is None: + self.gbs.Add(c, (self.row, col), flag=flag) + else: + self.gbs.Add(c, (self.row, col), span=(1, span), flag=flag) + return c + def AddTextCHelp(self, col, text, help_text, span=None): + bsizer = wx.BoxSizer(wx.HORIZONTAL) + txt = wx.StaticText(self, -1, text) + bsizer.Add(txt, flag=wx.ALIGN_CENTER_VERTICAL) + btn = QuiskPushbutton(self, self._BTnHelp, "..") + btn.SetColorGray() + btn.quisk_help_text = help_text + btn.quisk_caption = text + h = self.quisk_height + 2 + btn.SetSizeHints(h, h, h, h) + bsizer.Add(btn, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT|wx.RIGHT, border=self.charx) + if col < 0: + pass + elif span is None: + self.gbs.Add(bsizer, (self.row, col), flag = wx.ALIGN_CENTER) + else: + self.gbs.Add(bsizer, (self.row, col), span=(1, span), flag = wx.ALIGN_CENTER) + return bsizer + def AddTextLHelp(self, col, text, help_text, span=None): + bsizer = wx.BoxSizer(wx.HORIZONTAL) + btn = QuiskPushbutton(self, self._BTnHelp, "..") + btn.SetColorGray() + btn.quisk_help_text = help_text + btn.quisk_caption = text + h = self.quisk_height + 2 + btn.SetSizeHints(h, h, h, h) + bsizer.Add(btn, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT|wx.RIGHT, border=self.charx) + txt = wx.StaticText(self, -1, text) + bsizer.Add(txt, flag=wx.ALIGN_CENTER_VERTICAL) + if col < 0: + pass + elif span is None: + self.gbs.Add(bsizer, (self.row, col), flag = 0) + else: + self.gbs.Add(bsizer, (self.row, col), span=(1, span), flag = 0) + return bsizer + def AddTextColorChangeHelp(self, col, text, color, btn_text, handler, help_text, border=2, span1=1, span2=1, span3=1): + txt = wx.StaticText(self, -1, text) + self.gbs.Add(txt, (self.row, col), span=(1, span1), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx) + col += span1 + Cbar = wx.StaticText(self, -1, '') + h = self.quisk_height - 2 + Cbar.SetSizeHints(h, h, -1, h) + if color: + Cbar.SetBackgroundColour(color) + self.gbs.Add(Cbar, (self.row, col), span=(1, span2), + flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.RIGHT, + border=self.charx*2//10) + col += span2 + btn = QuiskPushbutton(self, handler, btn_text) + btn.SetColorGray() + h = self.quisk_height + 2 + btn.SetSizeHints(-1, h, -1, h) + self.gbs.Add(btn, (self.row, col), span=(1, span3), flag = wx.EXPAND) + col += span3 + hbtn = QuiskPushbutton(self, self._BTnHelp, "..") + hbtn.SetColorGray() + hbtn.quisk_help_text = help_text + hbtn.quisk_caption = text + h = self.quisk_height + 2 + hbtn.SetSizeHints(h, h, h, h) + self.gbs.Add(hbtn, (self.row, col), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx*border) + return txt, Cbar, btn + def AddTextEditHelp(self, col, text1, text2, help_text, border=2, span1=1, span2=1, no_edit=True): + txt = wx.StaticText(self, -1, text1) + self.gbs.Add(txt, (self.row, col), span=(1, span1), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx) + col += span1 + #txt = wx.StaticText(self, -1, text2) + if no_edit: + edt = QuiskTextCtrl(self, text2, style=wx.TE_READONLY) + else: + edt = QuiskTextCtrl(self, text2, style=wx.TE_PROCESS_ENTER) + #self.gbs.Add(txt, (self.row, col), span=(1, span2), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx) + self.gbs.Add(edt, (self.row, col), span=(1, span2), + flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.RIGHT, + border=self.charx*2//10) + col += span2 + btn = QuiskPushbutton(self, self._BTnHelp, "..") + btn.SetColorGray() + btn.quisk_help_text = help_text + btn.quisk_caption = text1 + h = self.quisk_height + 2 + btn.SetSizeHints(h, h, h, h) + self.gbs.Add(btn, (self.row, col), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx*border) + return txt, edt, btn + def AddTextSliderHelp(self, col, text, value, themin, themax, handler, help_text, border=2, span=1, scale=1): + display = "%" in text + sld = SliderBoxHH(self, text, value, themin, themax, handler, display, scale) + self.gbs.Add(sld, (self.row, col), span=(1, span), + flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.RIGHT, + border=self.charx*2//10) + col += span + btn = QuiskPushbutton(self, self._BTnHelp, "..") + btn.SetColorGray() + btn.quisk_help_text = help_text + btn.quisk_caption = text + h = self.quisk_height + 2 + btn.SetSizeHints(h, h, h, h) + self.gbs.Add(btn, (self.row, col), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx*border) + return sld, btn + def AddPopupMenuHelp(self, Qclass, col, text, btn_text, help_text, span1=1, span2=1, border=1): + if text: + txt = wx.StaticText(self, -1, text) + self.gbs.Add(txt, (self.row, col), span=(1, span1), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx) + col += span1 + ctrl = Qclass(self, btn_text, self.quisk_height - 2, style=wx.BORDER_RAISED) + self.gbs.Add(ctrl, (self.row, col), span=(1, span2), flag = wx.EXPAND|wx.RIGHT|wx.TOP|wx.BOTTOM|wx.LEFT, border=2) + col += span2 + self.AddHelpButton(col, text, help_text, border) + return ctrl + def AddTextButtonHelp(self, col, text, butn_text, handler, help_text, span1=1, span2=1, border=1): + if text: + txt = wx.StaticText(self, -1, text) + self.gbs.Add(txt, (self.row, col), span=(1, span1), flag = 0) + col += span1 + else: + txt = None + btn = QuiskPushbutton(self, handler, butn_text) + btn.SetColorGray() + h = self.quisk_height + 2 + btn.SetSizeHints(-1, h, -1, h) + self.gbs.Add(btn, (self.row, col), span=(1, span2), flag = wx.EXPAND) + col += span2 + hbtn = QuiskPushbutton(self, self._BTnHelp, "..") + hbtn.SetColorGray() + hbtn.quisk_help_text = help_text + hbtn.quisk_caption = text + h = self.quisk_height + 2 + hbtn.SetSizeHints(h, h, h, h) + self.gbs.Add(hbtn, (self.row, col), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx*border) + return txt, btn + def AddText2CheckHelp(self, col, text, butn1_text, handler1, butn2_text, handler2, help_text, span1=1, span2=1, border=1): + if text: + txt = wx.StaticText(self, -1, text) + self.gbs.Add(txt, (self.row, col), span=(1, span1), flag = wx.ALIGN_CENTER_VERTICAL | wx.EXPAND) + col += span1 + else: + txt = None + btn1 = QuiskCheckbutton(self, handler1, butn1_text) + btn1.SetColorGray() + btn2 = QuiskCheckbutton(self, handler2, butn2_text) + btn2.SetColorGray() + w1 = btn1.GetSize().Width + w2 = btn2.GetSize().Width + w = max(w1, w2) + h = self.quisk_height + 2 + btn1.SetSizeHints(w, h, w, h) + btn2.SetSizeHints(w, h, w, h) + bsizer = wx.BoxSizer(wx.HORIZONTAL) + bsizer.Add(btn1, proportion=1) + bsizer.Add(2, h, proportion=0) + bsizer.Add(btn2, proportion=1) + self.gbs.Add(bsizer, (self.row, col), span=(1, span2), flag = wx.EXPAND) + col += span2 + hbtn = QuiskPushbutton(self, self._BTnHelp, "..") + hbtn.SetColorGray() + hbtn.quisk_help_text = help_text + hbtn.quisk_caption = text + h = self.quisk_height + 2 + hbtn.SetSizeHints(h, h, h, h) + self.gbs.Add(hbtn, (self.row, col), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx*border) + return txt, btn1, btn2 + def AddText2ButtonHelp(self, col, text, butn1_text, handler1, butn2_text, handler2, help_text, span1=1, span2=1, border=1): + if text: + txt = wx.StaticText(self, -1, text) + self.gbs.Add(txt, (self.row, col), span=(1, span1), flag = 0) + col += span1 + else: + txt = None + btn1 = QuiskPushbutton(self, handler1, butn1_text) + btn1.SetColorGray() + btn2 = QuiskPushbutton(self, handler2, butn2_text) + btn2.SetColorGray() + w1 = btn1.GetSize().Width + w2 = btn2.GetSize().Width + w = max(w1, w2) + h = self.quisk_height + 2 + btn1.SetSizeHints(w, h, 999, h) + btn2.SetSizeHints(w, h, 999, h) + bsizer = wx.BoxSizer(wx.HORIZONTAL) + bsizer.Add(btn1, proportion=1) + bsizer.Add(2, h, proportion=0) + bsizer.Add(btn2, proportion=1) + self.gbs.Add(bsizer, (self.row, col), span=(1, span2), flag = wx.EXPAND) + col += span2 + hbtn = QuiskPushbutton(self, self._BTnHelp, "..") + hbtn.SetColorGray() + hbtn.quisk_help_text = help_text + hbtn.quisk_caption = text + h = self.quisk_height + 2 + hbtn.SetSizeHints(h, h, h, h) + self.gbs.Add(hbtn, (self.row, col), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx*border) + return txt, btn1, btn2 + def AddTextCtrl(self, col, text, handler=None, span=None): + c = wx.TextCtrl(self, -1, text, style=wx.TE_RIGHT) + if col < 0: + pass + elif span is None: + self.gbs.Add(c, (self.row, col), flag=wx.ALIGN_CENTER) + else: + self.gbs.Add(c, (self.row, col), span=(1, span), flag=wx.ALIGN_CENTER) + if handler: + c.Bind(wx.EVT_TEXT, handler) + return c + def AddBoxSizer(self, col, span): + bsizer = wx.BoxSizer(wx.HORIZONTAL) + self.gbs.Add(bsizer, (self.row, col), span=(1, span)) + return bsizer + def AddColSpacer(self, col, width): # add a width spacer to row 0 + self.gbs.Add((width * self.charx, 1), (0, col)) # width is in characters + def AddRadioButton(self, col, text, handler=None, span=None, start=False): + if start: + c = wx.RadioButton(self, -1, text, style=wx.RB_GROUP) + else: + c = wx.RadioButton(self, -1, text) + if col < 0: + pass + elif span is None: + self.gbs.Add(c, (self.row, col), flag=wx.ALIGN_CENTER_VERTICAL) + else: + self.gbs.Add(c, (self.row, col), span=(1, span), flag=wx.ALIGN_CENTER_VERTICAL) + if handler: + c.Bind(wx.EVT_RADIOBUTTON, handler) + return c + def AddCheckBox(self, col, text, handler=None, flag=0, border=0): + btn = wx.CheckBox(self, -1, text) + h = self.quisk_height + 2 + btn.SetSizeHints(-1, h, -1, h) + if col >= 0: + self.gbs.Add(btn, (self.row, col), flag=flag, border=border*self.charx) + if self.radio_name == "ConfigFileRadio": + btn.Enable(False) + noname_enable.append(btn) + if handler: + btn.Bind(wx.EVT_CHECKBOX, handler) + return btn + def AddBitField(self, col, number, name, band, value, handler=None, span=None, border=1): + bf = QuiskBitField(self, number, value, self.quisk_height, handler) + if col < 0: + pass + elif span is None: + self.gbs.Add(bf, (self.row, col), flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL|wx.RIGHT|wx.LEFT, border=border*self.charx) + else: + self.gbs.Add(bf, (self.row, col), span=(1, span), flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL|wx.RIGHT|wx.LEFT, border=border*self.charx) + bf.quisk_data_name = name + bf.quisk_band = band + return bf + def AddPushButton(self, col, text, handler, border=0): + btn = QuiskPushbutton(self, handler, text) + btn.SetColorGray() + h = self.quisk_height + 2 + btn.SetSizeHints(-1, h, -1, h) + if col >= 0: + self.gbs.Add(btn, (self.row, col), flag=wx.RIGHT|wx.LEFT, border=border*self.charx) + if self.radio_name == "ConfigFileRadio": + btn.Enable(False) + noname_enable.append(btn) + return btn + def AddPushButtonR(self, col, text, handler, border=0): + btn = self.AddPushButton(-1, text, handler, border) + if col >= 0: + self.gbs.Add(btn, (self.row, col), flag=wx.ALIGN_RIGHT|wx.RIGHT|wx.LEFT, border=border*self.charx) + return btn + def AddComboCtrl(self, col, value, choices, right=False, no_edit=False, span=None, border=1): + if no_edit: + cb = ChoiceCombo(self, value, choices) + else: + cb = ComboCtrl(self, value, choices) + if col < 0: + pass + elif span is None: + self.gbs.Add(cb, (self.row, col), flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.RIGHT|wx.LEFT, border=border*self.charx) + else: + self.gbs.Add(cb, (self.row, col), span=(1, span), flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.RIGHT|wx.LEFT, border=border*self.charx) + if self.radio_name == "ConfigFileRadio": + cb.Enable(False) + noname_enable.append(cb) + return cb + def AddComboCtrlTx(self, col, text, value, choices, right=False, no_edit=False): + c = wx.StaticText(self, -1, text) + if col >= 0: + self.gbs.Add(c, (self.row, col)) + cb = self.AddComboCtrl(col + 1, value, choices, right, no_edit) + else: + cb = self.AddComboCtrl(col, value, choices, right, no_edit) + return c, cb + def AddTextComboHelp(self, col, text, value, choices, help_text, no_edit=False, border=2, span_text=1, span_combo=1): + txt = wx.StaticText(self, -1, text) + self.gbs.Add(txt, (self.row, col), span=(1, span_text), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx) + col += span_text + cb = self.AddComboCtrl(-1, value, choices, False, no_edit) + if no_edit: + if '#' in value: + value = value[0:value.index('#')] + value = value.strip() + l = len(value) + for i in range(len(choices)): + ch = choices[i] + if '#' in ch: + ch = ch[0:ch.index('#')] + ch.strip() + if value == ch[0:l]: + cb.SetSelection(i) + break + else: + if 'fail' in value: + pass + else: + print ("Failure to set value for", text, value, choices) + self.gbs.Add(cb, (self.row, col), span=(1, span_combo), + flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.RIGHT, border=self.charx*2//10) + col += span_combo + btn = QuiskPushbutton(self, self._BTnHelp, "..") + btn.SetColorGray() + btn.quisk_help_text = help_text + btn.quisk_caption = text + h = self.quisk_height + 2 + btn.SetSizeHints(h, h, h, h) + self.gbs.Add(btn, (self.row, col), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx*border) + return txt, cb, btn + def AddTextDblSpinnerHelp(self, col, text, value, dmin, dmax, dinc, help_text, border=2, span_text=1, span_spinner=1): + txt = wx.StaticText(self, -1, text) + self.gbs.Add(txt, (self.row, col), span=(1, span_text), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx) + col += span_text + spn = wx.SpinCtrlDouble(self, -1, initial=value, min=dmin, max=dmax, inc=dinc) + self.gbs.Add(spn, (self.row, col), span=(1, span_spinner), + flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.RIGHT, + border=self.charx*2//10) + col += span_spinner + btn = QuiskPushbutton(self, self._BTnHelp, "..") + btn.SetColorGray() + btn.quisk_help_text = help_text + btn.quisk_caption = text + h = self.quisk_height + 2 + btn.SetSizeHints(h, h, h, h) + self.gbs.Add(btn, (self.row, col), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx*border) + return txt, spn, btn + def AddTextSpinnerHelp(self, col, text, value, imin, imax, help_text, border=2, span_text=1, span_spinner=1): + txt = wx.StaticText(self, -1, text) + self.gbs.Add(txt, (self.row, col), span=(1, span_text), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx) + col += span_text + spn = wx.SpinCtrl(self, -1, "") + spn.SetRange(imin, imax) + spn.SetValue(value) + self.gbs.Add(spn, (self.row, col), span=(1, span_spinner), + flag=wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.RIGHT, + border=self.charx*2//10) + col += span_spinner + btn = QuiskPushbutton(self, self._BTnHelp, "..") + btn.SetColorGray() + btn.quisk_help_text = help_text + btn.quisk_caption = text + h = self.quisk_height + 2 + btn.SetSizeHints(h, h, h, h) + self.gbs.Add(btn, (self.row, col), flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=self.charx*border) + return txt, spn, btn + def _BTnHelp(self, event): + btn = event.GetEventObject() + caption = btn.quisk_caption + i = caption.find('%') # remove formats %.2f + if i > 0: + caption = caption[0:i] + dlg = wx.MessageDialog(self, btn.quisk_help_text, caption, style=wx.OK|wx.ICON_INFORMATION) + dlg.ShowModal() + dlg.Destroy() + def ErrorCheck(self, ctrl): # Return True for OK + name = ctrl.quisk_data_name + value = ctrl.GetValue() + #if name == 'quisk_debug_sound' and value in ("New Sound", "Debug New Sound") and not QS.get_params('have_soundio'): + # dlg = wx.MessageDialog(None, "Please install the libsoundio-dev package, and re-make", 'New Sound', wx.OK|wx.ICON_ERROR) + # dlg.ShowModal() + # dlg.Destroy() + # return False + return True + def OnChange(self, ctrl): + value = ctrl.GetValue() + self.OnChange2(ctrl, value) + def OnChange2(self, ctrl, value): + # Careful: value is Unicode + name = ctrl.quisk_data_name + try: + fmt4 = local_conf.format4name[name] + except: + if name[0:4] in ("lin_", "win_"): + fmt4 = local_conf.format4name[name[4:]] + fmt4 = fmt4[0:4] + ok, x = self.EvalItem(value, fmt4) # Only evaluates integer, number, boolean, text, rfile + if ok: + radio_dict = local_conf.GetRadioDict(self.radio_name) + if name in name2format and name2format[name][1] == value: + if name in radio_dict: + del radio_dict[name] + local_conf.settings_changed = True + else: + radio_dict[name] = value + local_conf.settings_changed = True + # Immediate changes + if self.radio_name == Settings[1]: # changed for current radio + if name in ('hot_key_ptt_toggle', 'hot_key_ptt_if_hidden', 'keyupDelay', 'cwTone', 'pulse_audio_verbose_output', + 'start_cw_delay', 'start_ssb_delay', 'maximum_tx_secs', 'quisk_serial_cts', 'quisk_serial_dsr', + 'hot_key_ptt1', 'hot_key_ptt2', 'midi_ptt_toggle'): + setattr(conf, name, x) + application.ImmediateChange(name) + elif name[0:4] in ('lin_', 'win_'): + name = name[4:] + if name in ('quisk_serial_port', ): + setattr(conf, name, x) + application.ImmediateChange(name) + elif name == "reverse_tx_sideband": + setattr(conf, name, x) + QS.set_tx_audio(reverse_tx_sideband=x) + elif name == "dc_remove_bw": + setattr(conf, name, x) + QS.set_sparams(dc_remove_bw=x) + elif name == "digital_output_level": + setattr(conf, name, x) + QS.set_sparams(digital_output_level=x) + elif name[0:7] == 'hermes_': + if name == 'hermes_TxLNA_dB': + application.Hardware.ChangeTxLNA(x) + elif name == "hermes_bias_adjust" and self.HermesBias0: + self.HermesBias0.Enable(x) + self.HermesBias1.Enable(x) + self.HermesWriteBiasButton.Enable(x) + application.Hardware.EnableBiasChange(x) + elif hasattr(application.Hardware, "ImmediateChange"): + setattr(conf, name, x) + application.Hardware.ImmediateChange(name) + def FormatOK(self, value, fmt4): # Check formats integer, number, boolean + ok, v = self.EvalItem(value, fmt4) + return ok + def EvalItem(self, value, fmt4): # Return Python integer, number, boolean, text, rfile, keycode + # return is (item_is_ok, evaluated_item) + if fmt4 not in ('inte', 'numb', 'bool', 'keyc'): # only certain formats are evaluated + return True, value # text, rfile are returned by default + jj = value.find('#') + if jj > 0: + value = value[0:jj] + try: # only certain formats are evaluated + if fmt4 == 'inte': + v = int(value, base=0) + elif fmt4 == 'numb': + v = float(value) + elif fmt4 == 'bool': + if value == "True": + v = True + else: + v = False + elif fmt4 == 'keyc': # key code + if value == "None": + v = None + else: + v = eval(value) + v = int(v) + else: + raise ValueError + except: + #traceback.print_exc() + dlg = wx.MessageDialog(None, + "Can not set item with format %s to value %s" % (fmt4, value), + 'Change to item', wx.OK|wx.ICON_ERROR) + dlg.ShowModal() + dlg.Destroy() + return False, None + return True, v + def GetValue(self, name, radio_dict): + try: + value = radio_dict[name] + except: + pass + else: + return value + # Value was not in radio_dict. Get it from conf. There are values for platform win_data_name and lin_data_name. + # The win_ and lin_ names are not in conf. + try: + fmt = local_conf.format4name[name] + except: + fmt = '' # not all items in conf are in section_data or receiver_data + try: + if fmt == 'dict': # make a copy for this radio + value = {} + value.update(getattr(conf, name)) + elif fmt == 'list': # make a copy for this radio + value = getattr(conf, name)[:] + else: + value = str(getattr(conf, name)) + except: + return '' + else: + return value + +class BaseWindow(wx.ScrolledCanvas, ControlMixin): + def __init__(self, parent): + wx.ScrolledCanvas.__init__(self, parent) + ControlMixin.__init__(self, parent) + +class ConfigConfig(BaseWindow): + def __init__(self, parent, width): + BaseWindow.__init__(self, parent) + self.width = width + self.SetBackgroundColour(parent.bg_color) + self.num_cols = 7 + #self.MarkCols() + self.radio_name = None + # Choice (combo) box for decimation + lst = application.Hardware.VarDecimGetChoices() + if lst: + txt = application.Hardware.VarDecimGetLabel() + index = application.Hardware.VarDecimGetIndex() + else: + txt = "Variable decimation" + lst = ["None"] + index = 0 + help_text = "If your hardware supports different sample rates, choose one here. For SoftRock, \ +change the sample rate of your sound card." + txt, cb, btn = self.AddTextComboHelp(1, txt, lst[index], lst, help_text, True) + if lst: + cb.handler = application.OnBtnDecimation + self.btn_decimation = cb + self.NextRow() + self.NextRow() + help_text = "SoftRock and other radios that use a sound card for Rx and Tx will need small corrections \ +to the amplitude and phase. Click here for an adjustment screen." + txt, btn = self.AddTextButtonHelp(1, "Adjust receive amplitude and phase", "Rx Phase..", self.OnBtnPhase, help_text) + self.rx_phase = btn + if not conf.name_of_sound_capt: + btn.Enable(0) + self.NextRow() + txt, btn = self.AddTextButtonHelp(1, "Adjust transmit amplitude and phase", "Tx Phase..", self.OnBtnPhase, help_text) + self.tx_phase = btn + if not conf.name_of_mic_play: + btn.Enable(0) + self.NextRow() + help_text = "There is a color bar under the X-axis showing the band plan. Click here for a screen to change the colors." + txt, btn = self.AddTextButtonHelp(1, "Colors on X-axis for CW, Phone", "Band plan..", self.OnBtnBandPlan, help_text) + self.NextRow() + help_text = "Click here to configure the link to WSJT-X." + txt, btn = self.AddTextButtonHelp(1, "Configure WSJT-X", "WSJT-X..", self.OnConfigureWsjtx, help_text) + self.NextRow() + lst = ("Never", "Main Rx0 on startup", "Main Rx0 now") + value = application.local_conf.globals.get("start_wsjtx", "Never") + txt, cb, btn = self.AddTextComboHelp(1, "Start WSJT-X", value, lst, help_text, True) + cb.handler = application.OnStartWsjtx + self.NextRow() + help_text = "This controls the Tx level for non-digital modes. It is usually 100%." + c1, btn = self.AddTextSliderHelp(1, "Tx level %d%% ", 100, 0, 100, self.OnTxLevel, help_text, span=2) + self.NextRow() + level = conf.digital_tx_level + help_text = "This controls the TX level for digital modes. Digital modes require greater linearity, and the digital level is often 25%." + c2, btn = self.AddTextSliderHelp(1, "Digital Tx level %d%% ", level, 0, level, self.OnDigitalTxLevel, help_text, span=2) + if not hasattr(application.Hardware, "SetTxLevel"): + c1.slider.Enable(0) + c2.slider.Enable(0) + self.NextRow() + #### Make controls SECOND column + self.row = 3 + self.AddTextL(4, "Configuration for the file record button: " + conf.Xbtn_text_file_rec, span=2) + self.NextRow() + # File for recording speaker audio + text = "Record Rx audio to WAV files 1, 2, ... " + path = application.file_name_rec_audio + help_text = 'These check buttons control what happens when you press the "File Record" button. \ +Please choose a directory for your recordings; perhaps the "Music" directory on your computer. \ +Then choose your file name. This "Record Rx audio" file is used for recording the radio speaker sound. \ +Recording stops when the button is released. When pressed again new files are created with names 001, 002, etc. \ +You will need to delete recordings you no longer need.' + self.file_button_rec_speaker = self.MakeFileButton(text, path, 0, 'rec_audio', help_text) + # File for recording samples + text = "Record I/Q samples to WAV files 1, 2, ... " + path = application.file_name_rec_samples + help_text = 'This file is used to record the I/Q samples and it works like the "Record Rx audio" button.' + self.file_button_rec_iq = self.MakeFileButton(text, path, 1, 'rec_samples', help_text) + # File for recording the microphone + text = "Record the mic to make a CQ message" + path = application.file_name_rec_mic + help_text = 'This file is used to record the microphone. It can be used to record a CQ message that can be played \ +over and over until someone answers. Change the "Play audio" to the same file to test the recording. Keep recording and \ +playing until you are satisfied, and then un-check the "Record mic" button. The file is replaced when the Record button \ +is pressed again (no 001, 002, ...).' + self.file_button_rec_mic = self.MakeFileButton(text, path, 2, 'rec_mic', help_text) + ## Play buttons + self.AddTextL(4, "Configuration for the file play button: " + conf.Xbtn_text_file_play, span=2) + self.NextRow() + # File for playing speaker audio + text = "Play audio from a WAV file" + path = application.file_name_play_audio + help_text = 'These record check buttons control what happens when you press the "File Play" button. \ +This button plays normal audio files, not samples.' + self.file_button_play_speaker = self.MakeFileButton(text, path, 10, 'play_audio', help_text) + # file for playing samples + text = "Receive saved I/Q samples from a file" + path = application.file_name_play_samples + help_text = 'This button plays I/Q samples. You can tune in different stations from the I/Q recording.' + self.file_button_play_iq = self.MakeFileButton(text, path, 11, 'play_samples', help_text) + # File for playing a file to the mic input for a CQ message + text = "Repeat a CQ message until a station answers" + path = application.file_name_play_cq + help_text = "This button enables the File Play button to start playing your CQ message." + self.file_button_play_mic = self.MakeFileButton(text, path, 12, 'play_cq', help_text) + # CQ repeat time + help_text = 'This is the amount of time to wait for someone to respond to your CQ message. \ +The CW message will then repeat. A time of zero means no repeat.' + sl, btn = self.AddTextSliderHelp(4, " Repeat secs %.1f ", 0, 0, 100, self.OnPlayFileRepeat, help_text, span=2, scale=0.1) + self.NextRow() + self.FitInside() + self.SetScrollRate(1, 1) + def MakeFileButton(self, text, path, index, name, help_text): + if index < 10: # record buttons + cb = self.AddCheckBox(4, text, self.OnCheckRecPlay) + elif index == 10: + cb = self.AddRadioButton(4, text, self.OnCheckRecPlay, start=True) + else: + cb = self.AddRadioButton(4, text, self.OnCheckRecPlay, start=False) + txt, b = self.AddTextButtonHelp(5, '', "File..", self.OnBtnFileName, help_text) + b.check_box = cb + b.index = cb.index = index + b.path = cb.path = path + b.name = 'file_name_' + name + if index < 10: # record buttons + if path: + enable = True + if index == 0: # check record audio if there is a path + cb.SetValue(True) + else: + enable = False + else: # playback buttons + enable = os.path.isfile(path) + cb.Enable(enable) + self.NextRow() + return b + def OnTxLevel(self, event): + application.tx_level = event.GetEventObject().GetValue() + application.Hardware.SetTxLevel() + def OnDigitalTxLevel(self, event): + application.digital_tx_level = event.GetEventObject().GetValue() + application.Hardware.SetTxLevel() + def OnBtnPhase(self, event): + btn = event.GetEventObject() + if btn.GetLabel()[0:2] == 'Tx': + rx_tx = 'tx' + else: + rx_tx = 'rx' + application.screenBtnGroup.SetLabel('Graph', do_cmd=True) + if application.w_phase: + application.w_phase.Raise() + else: + application.w_phase = QAdjustPhase(self, self.width, rx_tx) + def OnBtnFileName(self, event): + btn = event.GetEventObject() + dr, fn = os.path.split(btn.path) + if btn.index in (0, 1): # record audio or samples + dlg = wx.FileDialog(self, 'Choose WAV file', dr, fn, style=wx.FD_SAVE, wildcard="Wave files (*.wav)|*.wav") + elif btn.index == 2: # record mic + dlg = wx.FileDialog(self, 'Choose WAV file', dr, fn, style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT, wildcard="Wave files (*.wav)|*.wav") + else: # play buttons + dlg = wx.FileDialog(self, 'Choose WAV file', dr, fn, style=wx.FD_OPEN, wildcard="Wave files (*.wav)|*.wav") + if dlg.ShowModal() == wx.ID_OK: + path = dlg.GetPath() + if path[-4:].lower() != '.wav': + path = path + '.wav' + btn.path = path + setattr(application, btn.name, path) + btn.check_box.path = path + btn.check_box.Enable(True) + if btn.index >= 10: # play buttons + btn.check_box.SetValue(True) + application.file_play_source = btn.index + self.EnableRecPlay() + dlg.Destroy() + def ChangePlayFile(self, btn, path): + btn.path = path + setattr(application, btn.name, path) + btn.check_box.path = path + btn.check_box.Enable(True) + self.EnableRecPlay() + def OnBtnBandPlan(self, ctrl): + from configure import BandPlanDlg + dlg = BandPlanDlg(self) + dlg.ShowModal() + dlg.Destroy() + def OnConfigureWsjtx(self, ctrl): + from configure import WsjtxDlg + dlg = WsjtxDlg(self) + dlg.ShowModal() + dlg.Destroy() + def InitRecPlay(self): + for btn in (self.file_button_play_speaker, self.file_button_play_iq, self.file_button_play_mic): + if btn.index == application.file_play_source: + btn.check_box.SetValue(True) + def EnableRecPlay(self): # Enable or disable file record/play buttons on main screen + enable_rec = False + enable_play = False + for btn in (self.file_button_rec_speaker, self.file_button_rec_iq, self.file_button_rec_mic): + if btn.check_box.GetValue(): + enable_rec = True + break + for btn in (self.file_button_play_speaker, self.file_button_play_iq, self.file_button_play_mic): + if btn.check_box.GetValue() and os.path.isfile(btn.path): + enable_play = True + break + application.btn_file_record.Enable(enable_rec) + application.btnFilePlay.Enable(enable_play) + def OnCheckRecPlay(self, event): + btn = event.GetEventObject() + if btn.GetValue(): + if btn.index >= 10: # play button + application.file_play_source = btn.index + self.EnableRecPlay() + def OnPlayFileRepeat(self, event): + application.file_play_repeat = event.GetEventObject().GetValue() * 0.1 + def OnFilePlayButton(self, play): + if play: + for btn in (self.file_button_play_speaker, self.file_button_play_iq, self.file_button_play_mic): + if application.file_play_source == btn.index and btn.check_box.GetValue() and os.path.isfile(btn.path): + QS.open_wav_file_play(btn.path) + break + else: + QS.set_file_name(play_button=0) # Close all play files + def OnFileRecordButton(self, record): # The File Record button on the main screen + if record: + for btn in (self.file_button_rec_speaker, self.file_button_rec_iq, self.file_button_rec_mic): + if btn.check_box.GetValue(): # open this file + if btn.index in (0, 1) and os.path.isfile(btn.path): # Change path + direc, fname = os.path.split(btn.path) + base = os.path.splitext(fname)[0] + while base and base[-1] in '0123456789': + base = base[0:-1] + if not base: + base = "rec" + index = 0 + for dir_entry in os.scandir(direc): + if dir_entry.is_file(): + name = dir_entry.name + if len(name) > 4 and name[-4] == '.' and name.startswith(base): + ma = re.match(base + '([0-9]+)[.]', name) + if ma: + index = max(index, int(ma.group(1), base=10)) + btn.path = os.path.join(direc, "%s%03d.wav" % (base, index + 1)) + QS.set_file_name(btn.index, btn.path, record_button=1) + if btn.index == 0: # Change play files to equal record files + self.ChangePlayFile(self.file_button_play_speaker, btn.path) + elif btn.index == 1: + self.ChangePlayFile(self.file_button_play_iq, btn.path) + else: + QS.set_file_name(record_button=0) # Close all record files + +class ConfigTxAudio(BaseWindow): + def __init__(self, parent): + BaseWindow.__init__(self, parent) + self.tmp_playing = False + self.timer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.OnTimer) + self.SetBackgroundColour(parent.bg_color) + self.num_cols = 4 + #self.MarkCols() + self.NextRow() + t = "This is a test screen for transmit audio. SSB, AM and FM have separate settings." + item = self.AddTextL(1, t, span=2) + self.NextRow() + self.NextRow() + self.NextRow() + help_text = "Record the transmit audio while adjusting clip and preemphasis; then play back the result." + txt, self.btn_record, self.btn_playback = self.AddText2CheckHelp(1, "Record transmit audio" + ' ' * 10, + "Record", self.OnBtnRecord, "Playback", self.OnBtnPlayback, help_text) + self.btn_playback.Enable(0) + if not conf.microphone_name: + self.btn_record.Enable(0) + self.NextRow() + help_text = "Audio level that triggers VOX (all modes)." + self.AddTextSliderHelp(1, "VOX level %d dB", application.levelVOX, -40, 0, application.OnLevelVOX, help_text, span=2) + self.NextRow() + help_text = "Time to hold VOX after end of audio in seconds." + self.AddTextSliderHelp(1, "VOX hold %0.2f secs", application.timeVOX, 0, 4000, application.OnTimeVOX, help_text, span=2, scale=0.001) + self.NextRow() + help_text = "Tx audio clipping level in dB for this mode." + sld, btn = self.AddTextSliderHelp(1, "Clip level %2d dB", 0, 0, 20, application.OnTxAudioClip, help_text, span=2) + application.CtrlTxAudioClip = sld + self.NextRow() + help_text = "Tx audio preemphasis of high frequencies." + sld, btn = self.AddTextSliderHelp(1, "Preemphasis %4.2f", 0, 0, 100, application.OnTxAudioPreemph, help_text, span=2, scale=0.01) + application.CtrlTxAudioPreemph = sld + self.NextRow() + self.FitInside() + self.SetScrollRate(1, 1) + def OnTimer(self, event): + if not self.tmp_playing: + self.timer.Stop() + elif QS.set_record_state(-1): # poll to see if playback is finished + self.tmp_playing = False + self.timer.Stop() + self.btn_playback.SetValue(False) + self.btn_record.Enable(1) + def OnBtnRecord(self, event): + if event.GetEventObject().GetValue(): + QS.set_kill_audio(1) + self.btn_playback.Enable(0) + QS.set_record_state(4) + else: + QS.set_kill_audio(0) + self.btn_playback.Enable(1) + QS.set_record_state(1) + def OnBtnPlayback(self, event): + if event.GetEventObject().GetValue(): + self.btn_record.Enable(0) + QS.set_record_state(2) + self.tmp_playing = True + self.timer.Start(milliseconds=200) + else: + self.btn_record.Enable(1) + QS.set_record_state(3) + self.tmp_playing = False + +class Radios(BaseWindow): # The "Radios" first-level page + def __init__(self, parent): + BaseWindow.__init__(self, parent) + self.SetBackgroundColour(parent.bg_color) + self.num_cols = 8 + self.radio_name = None + self.cur_radio_text = self.AddTextL(1, 'xx', self.num_cols - 1) + self.SetCurrentRadioText() + self.NextRow() + self.NextRow() + item = self.AddTextL(1, "When Quisk starts, use the radio") + self.start_radio = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True) + self.start_radio.handler = self.OnChoiceStartup + self.NextRow() + item = self.AddTextL(1, "Add a new radio with the general type") + choices = [] + for name, data in local_conf.receiver_data: + choices.append(name) + self.add_type = self.AddComboCtrl(2, '', choices=choices, no_edit=True) + self.add_type.SetSelection(0) + item = self.AddTextL(3, "and name the new radio") + self.add_name = self.AddComboCtrl(4, '', choices=["My Radio", "SR with XVtr", "SoftRock"]) + item = self.AddPushButton(5, "Add", self.OnBtnAdd) + self.NextRow() + item = self.AddTextL(1, "Rename the radio named") + self.rename_old = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True) + item = self.AddTextL(3, "to the new name") + self.rename_new = self.AddComboCtrl(4, '', choices=["My Radio", "SR with XVtr", "SoftRock"]) + item = self.AddPushButton(5, "Rename", self.OnBtnRename) + self.NextRow() + item = self.AddTextL(1, "Delete the radio named") + self.delete_name = self.AddComboCtrl(2, 'big_radio_name', choices=[], no_edit=True) + item = self.AddPushButton(3, "Delete", self.OnBtnDelete) + self.NextRow() + self.FitInside() + self.SetScrollRate(1, 1) + self.NewRadioNames() + def SetCurrentRadioText(self): + radio_dict = local_conf.GetRadioDict(self.radio_name) + radio_type = radio_dict['hardware_file_type'] + if Settings[1] == "ConfigFileRadio": + text = 'The current radio is ConfigFileRadio, so all settings come from the config file. The hardware type is %s.' % radio_type + else: + text = "Quisk is running with settings from the radio %s. The hardware type is %s." % (Settings[1], radio_type) + self.cur_radio_text.SetLabel(text) + def DuplicateName(self, name): + if name in Settings[2] or name == "ConfigFileRadio": + dlg = wx.MessageDialog(self, "The name already exists. Please choose a different name.", + 'Quisk', wx.OK) + dlg.ShowModal() + dlg.Destroy() + return True + return False + def OnBtnAdd(self, event): + name = self.add_name.GetValue().strip() + if not name or self.DuplicateName(name): + return + self.add_name.SetValue('') + typ = self.add_type.GetValue().strip() + if local_conf.AddRadio(name, typ): + if Settings[0] != "Ask me": + Settings[0] = name + self.NewRadioNames() + local_conf.settings_changed = True + def OnBtnRename(self, event): + old = self.rename_old.GetValue() + new = self.rename_new.GetValue().strip() + if not old or not new or self.DuplicateName(new): + return + self.rename_new.SetValue('') + if local_conf.RenameRadio(old, new): + if old == 'ConfigFileRadio' and Settings[1] == "ConfigFileRadio": + Settings[1] = new + elif Settings[1] == old: + Settings[1] = new + self.SetCurrentRadioText() + if Settings[0] != "Ask me": + Settings[0] = new + self.NewRadioNames() + local_conf.settings_changed = True + def OnBtnDelete(self, event): + name = self.delete_name.GetValue() + if not name: + return + dlg = wx.MessageDialog(self, + "Are you sure you want to permanently delete the radio %s?" % name, + 'Quisk', wx.OK|wx.CANCEL|wx.ICON_EXCLAMATION) + ret = dlg.ShowModal() + dlg.Destroy() + if ret == wx.ID_OK and local_conf.DeleteRadio(name): + self.NewRadioNames() + local_conf.settings_changed = True + def OnChoiceStartup(self, ctrl): + choice = self.start_radio.GetValue() + if Settings[0] != choice: + Settings[0] = choice + local_conf.settings_changed = True + def NewRadioNames(self): # Correct all choice lists for changed radio names + choices = Settings[2][:] # can rename any available radio + self.rename_old.SetItems(choices) + self.rename_old.SetSelection(0) + if "ConfigFileRadio" in choices: + choices.remove("ConfigFileRadio") + if Settings[1] in choices: + choices.remove(Settings[1]) + self.delete_name.SetItems(choices) # can not delete ConfigFileRadio nor the current radio + self.delete_name.SetSelection(0) + choices = Settings[2] + ["Ask me"] + if "ConfigFileRadio" not in choices: + choices.append("ConfigFileRadio") + self.start_radio.SetItems(choices) # can start any radio, plus "Ask me" and "ConfigFileRadio" + try: # Set text in control + index = choices.index(Settings[0]) # last used radio, or new or renamed radio + except: + num = len(Settings[2]) + if len == 0: + index = 1 + elif num == 1: + index = 0 + else: + index = len(choices) - 2 + Settings[0] = choices[index] + self.start_radio.SetSelection(index) + +class RadioSection(BaseWindow): # The pages for each section in the second-level notebook for each radio + help_cw = \ + 'If your CW key is not connected to your radio hardware, you can connect the key to Quisk using a serial port or MIDI. '\ + 'For the serial port, enter the serial port name and either CTS or DSR for the key. '\ + 'You can use MIDI for keying. See the "Keys" screen . '\ + 'If you turn on the Quisk internal sidetone, be sure to use the Fast Sound setting for Windows. '\ + 'And for Linux, use Alsa for the radio sound output. '\ + 'Reduce the hardware poll time for faster response.' + def __init__(self, parent, radio_name, section, names): + BaseWindow.__init__(self, parent) + self.radio_name = radio_name + self.section = section + self.names = names + self.MakeControls() + def MakeControls(self): + self.num_cols = 8 + #self.MarkCols() + self.NextRow(3) + Keys = self.section == "Keys" + if Keys: + col = 5 + else: + col = 1 + start_row = self.row + radio_dict = local_conf.GetRadioDict(self.radio_name) + radio_type = radio_dict['hardware_file_type'] + for name, text, fmt, help_text, values in self.names: + if name == 'remote_radio_password': + self.AddTextButtonHelp(col, text, "Change", self.OnChangePassword, help_text) + if col == 1: + col = 4 + else: + col = 1 + self.NextRow() + elif name == 'remote_radio_ip' and radio_type != "Control Head": + continue + elif name == 'favorites_file_path': + self.favorites_path = radio_dict.get('favorites_file_path', '') + row = self.row + self.row = 1 + item, self.favorites_combo, btn = self.AddTextComboHelp(1, text, self.favorites_path, values, help_text, False, span_text=1, span_combo=4) + self.favorites_combo.handler = self.OnButtonChangeFavorites + item = self.AddPushButtonR(7, "Change..", self.OnButtonChangeFavorites, border=0) + self.row = row + else: + if name == "keyupDelay": + if col != 1: + col = 1 + self.NextRow() + self.NextRow() + self.NextRow() + self.AddTextCHelp(1, "CW Settings for Remote and Local Operation", self.help_cw, 8) + col = 1 + self.NextRow() + self.NextRow() + if fmt[0:4] in ('dict', 'list'): + continue + if name[0:4] == platform_ignore: + continue + if name == 'use_fast_sound' and sys.platform != 'win32': + continue + value = self.GetValue(name, radio_dict) + no_edit = "choice" in fmt or fmt == 'boolean' + if name == "midi_cwkey_device": + # Start of MIDI + values = QS.control_midi(get_in_names=1) + values.insert(0, '') + txt, cb, btn = self.AddTextComboHelp(col, text, value, values, help_text, no_edit) + cb.handler = self.OnChange + cb.quisk_data_name = name + if Keys: + self.NextRow() + elif col == 1: + col = 4 + else: + col = 1 + self.NextRow() + if self.section == "Keys": + if self.radio_name == Settings[1]: # Current radio in use + application.config_midi_window = self + charx = self.charx + self.list_ctrl = list_ctrl = wx.ListCtrl(self, size=(charx * 19 + 5, self.quisk_height * 8), + style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.BORDER_SIMPLE) + list_ctrl.InsertColumn(0, 'Midi', width=charx*10) + list_ctrl.InsertColumn(1, 'Quisk', width=charx*9) + list_ctrl.SetBackgroundColour(wx.Colour(0xeaeaea)) + list_ctrl.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnMidiListSelected) + index = 0 + for txt_note in local_conf.MidiNoteDict: # txt_note is a string + int_note = int(txt_note, base=0) + self.list_ctrl.InsertItem(index, txt_note) + self.list_ctrl.SetItemData(index, int_note) + self.list_ctrl.SetItem(index, 1, local_conf.MidiNoteDict[txt_note]) + index += 1 + list_ctrl.SortItems(lambda x, y: x - y) + rows = self.row - start_row + 1 + self.row = start_row + self.gbs.Add(list_ctrl, (start_row, 1), span=(rows, 1), flag=wx.RIGHT, border=charx * 15 // 10) + #self.gbs.Add (charx, self.chary // 2, wx.GBPosition(start_row + rows, 1)) # Spacer at the bottom + hlp = \ + 'You can use a MIDI keyboard or other MIDI device to control Quisk. '\ + 'These Midi devices generate a control number and value for buttons, knobs and jog wheels. '\ + 'You can assign the number to a Quisk button or slider. '\ + 'Operate the control and look here to see the note or control number received by Quisk. '\ + 'Then press a button below to assign the control.' + txt, self.midi_edt, btn = self.AddTextEditHelp(2, "Midi control", '', hlp, border=2, span1=1, span2=1, no_edit=True) + self.NextRow() + hlp = 'Assign the Midi button to a Quisk button.' + ctrl = self.AddPopupMenuHelp(MidiButton, 2, "Assign Midi control to", "Button", hlp) + ctrl.handler = self.OnMidiMenu + self.NextRow() + hlp = \ + 'A "knob" is a Midi control that rotates left and right one turn. It sends a value from 0 to 127. '\ + 'Assign the Midi knob to a Quisk control.' + ctrl = self.AddPopupMenuHelp(MidiKnob, 2, "Assign Midi control to", "Knob", hlp) + ctrl.handler = self.OnMidiMenu + self.NextRow() + hlp = \ + 'A "jog wheel" is a Midi control that rotates around and around. It sends continuous high or low values. '\ + 'Assign the Midi jog wheel to a Quisk control.' + self.midiJogWheel = self.AddPopupMenuHelp(MidiJogWheel, 2, "Assign Midi control to", "Jog Wheel", hlp) + self.midiJogWheel.handler = self.OnMidiMenu + self.NextRow() + hlp = "To delete a row, select it and press this button." + self.AddTextButtonHelp(2, "Delete Midi row", "Delete", self.OnMidiDelete, hlp) + self.NextRow() + if self.section == "Timing and CW": + if col != 1: + col = 1 + self.NextRow() + self.NextRow() + self.AddTextL(1, 'Midi CW key moved to "Keys"', span=4) + if not Keys: + self.AddColSpacer(2, 20) + self.AddColSpacer(5, 20) + self.FitInside() + self.SetScrollRate(1, 1) + def OnButtonChangeFavorites(self, event): + if isinstance(event, (ChoiceCombo, ComboCtrl)): + path = event.GetValue() + else: + direc, fname = os.path.split(getattr(conf, 'favorites_file_in_use')) + dlg = wx.FileDialog(None, "Choose Favorites File", direc, fname, "*.txt", wx.FD_OPEN) + if dlg.ShowModal() == wx.ID_OK: + path = dlg.GetPath() + self.favorites_combo.SetText(path) + dlg.Destroy() + else: + dlg.Destroy() + return + path = path.strip() + self.favorites_path = path + local_conf.GetRadioDict(self.radio_name)["favorites_file_path"] = path + local_conf.settings_changed = True + def OnMidiDelete(self, event): + ctrl = self.list_ctrl + index = ctrl.GetFirstSelected() + if index < 0: + return + txt_note = ctrl.GetItem(index, 0).GetText() + ctrl.DeleteItem(index) + del local_conf.MidiNoteDict[txt_note] + local_conf.settings_changed = True + if index > 0: + index -= 1 + if ctrl.GetItemCount() > 0: + ctrl.Select(index) + ctrl.EnsureVisible(index) + def MidiAddNote(self): + note = self.midi_edt.GetValue() + self.midi_edt.Clear() + if not note: + return -1 + txt_note = note.split()[0] + if txt_note[2] == '8': + txt_note = "0x9%s" % txt_note[3:] # Change Note Off to store as Note On + int_note = int(txt_note, base=0) + ctrl = self.list_ctrl + index = ctrl.FindItem(-1, txt_note) + if index < 0: # Item is not in the list. Add it. + ctrl.Append((txt_note, '')) + ctrl.SetItemData(ctrl.GetItemCount() - 1, int_note) + ctrl.SortItems(lambda x, y: x-y) + index = ctrl.FindItem(-1, txt_note) + local_conf.MidiNoteDict[txt_note] = '' + local_conf.settings_changed = True + if index >= 0: + ctrl.Select(index) + ctrl.EnsureVisible(index) + return index + def OnMidiMenu(self, ctrl, jog_tag=None): + if jog_tag: # Change the jog tag on the current line + index = self.list_ctrl.GetFirstSelected() + if index < 0: + return + text = self.list_ctrl.GetItem(index, 1).GetText() + if len(text) > 3 and text[-3] == ' ' and text[-2] in "+-" and text[-1] in '0123456789': # Item is a jog wheel item + text = text[0:-2] + jog_tag + else: # There is no jog tag on the current item + return + else: # Assign the Midi message to a Quisk control + index = self.MidiAddNote() + if index < 0: + return + text = ctrl.value + self.list_ctrl.SetItem(index, 1, text) + txt_note = self.list_ctrl.GetItem(index, 0).GetText() + local_conf.MidiNoteDict[txt_note] = text + local_conf.settings_changed = True + def OnMidiListSelected(self, event): + event.Skip() + self.midi_edt.Clear() + index = self.list_ctrl.GetFirstSelected() + text = self.list_ctrl.GetItem(index, 1).GetText() + if len(text) > 3 and text[-3] == ' ' and text[-2] in "+-" and text[-1] in '0123456789': # Item is a jog wheel item + self.midiJogWheel.ChangeSelection(text[-2:]) + def OnNewMidiNote(self, message): + txt_note = "0x%02X%02X %d" % tuple(message) + self.midi_edt.SetValue(txt_note) + if txt_note[2] == '8': # Note off. We already saw the Note On message. + return + txt_note = txt_note.split()[0] + index = self.list_ctrl.FindItem(-1, txt_note) + if index >= 0: + self.list_ctrl.Select(index) + self.list_ctrl.EnsureVisible(index) + else: + index = self.list_ctrl.GetFirstSelected() + if index >= 0: + self.list_ctrl.Select(index, 0) + def OnChangePassword(self, event): + text = local_conf.globals.get('remote_radio_password', '') + dlg = wx.TextEntryDialog(self, "Please enter a password for remote access", + "Password Entry", text) + if dlg.ShowModal() == wx.ID_OK: + local_conf.globals["remote_radio_password"] = dlg.GetValue() + local_conf.settings_changed = True + local_conf.SaveState() + +class RadioHardwareBase(BaseWindow): # The Hardware page in the second-level notebook for each radio + def __init__(self, parent, radio_name): + BaseWindow.__init__(self, parent) + self.radio_name = radio_name + self.num_cols = 8 + self.PMcalDialog = None + #self.MarkCols() + def AlwaysMakeControls(self): + radio_dict = local_conf.GetRadioDict(self.radio_name) + radio_type = radio_dict['hardware_file_type'] + data_names = local_conf.GetReceiverData(radio_type) + self.AddTextL(1, "These are the hardware settings for a radio of type %s" % radio_type, self.num_cols-1) + for name, text, fmt, help_text, values in data_names: + if name == 'hardware_file_name': + self.hware_path = self.GetValue(name, radio_dict) + row = self.row + self.row = 3 + item, self.hware_combo, btn = self.AddTextComboHelp(1, text, self.hware_path, values, help_text, False, span_text=1, span_combo=4) + self.hware_combo.handler = self.OnButtonChangeHardware + item = self.AddPushButtonR(7, "Change..", self.OnButtonChangeHardware, border=0) + elif name == 'widgets_file_name': + self.widgets_path = self.GetValue(name, radio_dict) + row = self.row + self.row = 5 + item, self.widgets_combo, btn = self.AddTextComboHelp(1, text, self.widgets_path, values, help_text, False, span_text=1, span_combo=4) + self.widgets_combo.handler = self.OnButtonChangeWidgets + item = self.AddPushButtonR(7, "Change..", self.OnButtonChangeWidgets, border=0) + self.NextRow(7) + self.AddColSpacer(2, 20) + self.AddColSpacer(5, 20) + self.SetScrollRate(1, 1) + def OnButtonChangeHardware(self, event): + if isinstance(event, (ChoiceCombo, ComboCtrl)): + path = event.GetValue() + else: + direc, fname = os.path.split(self.hware_path) + dlg = wx.FileDialog(None, "Choose Hardware File", direc, fname, "*.py", wx.FD_OPEN) + if dlg.ShowModal() == wx.ID_OK: + path = dlg.GetPath() + self.hware_combo.SetText(path) + dlg.Destroy() + else: + dlg.Destroy() + return + path = path.strip() + self.hware_path = path + local_conf.GetRadioDict(self.radio_name)["hardware_file_name"] = path + local_conf.settings_changed = True + def OnButtonChangeWidgets(self, event): + if isinstance(event, (ChoiceCombo, ComboCtrl)): + path = event.GetValue() + else: + direc, fname = os.path.split(self.widgets_path) + dlg = wx.FileDialog(None, "Choose Widgets File", direc, fname, "*.py", wx.FD_OPEN) + if dlg.ShowModal() == wx.ID_OK: + path = dlg.GetPath() + self.widgets_combo.SetText(path) + dlg.Destroy() + else: + dlg.Destroy() + return + path = path.strip() + self.widgets_path = path + local_conf.GetRadioDict(self.radio_name)["widgets_file_name"] = path + local_conf.settings_changed = True + +class RadioHardware(RadioHardwareBase): # The Hardware page in the second-level notebook for each radio + def __init__(self, parent, radio_name): + RadioHardwareBase.__init__(self, parent, radio_name) + self.AlwaysMakeControls() + self.HermesBias0 = None + self.HermesBias1 = None + radio_dict = local_conf.GetRadioDict(radio_name) + radio_type = radio_dict['hardware_file_type'] + data_names = local_conf.GetReceiverData(radio_type) + col = 1 + border = 2 + hermes_board_id = 0 + if radio_type == "Hermes": + try: + hermes_board_id = application.Hardware.hermes_board_id + except: + pass + if radio_name == Settings[1] and hasattr(application.Hardware, "ProgramGateware"): + help_text = "Choose an RBF file and program the Gateware (FPGA software) over Ethernet." + self.AddTextButtonHelp(1, "Gateware Update", "Program from RBF file..", application.Hardware.ProgramGateware, help_text) + col = 1 + self.NextRow(self.row + 2) + for name, text, fmt, help_text, values in data_names: + if name in ('hardware_file_name', 'widgets_file_name'): + pass + elif name[0:4] == platform_ignore: + pass + elif name in ('Hermes_BandDictEnTx', ): + pass + elif 'Hl2_' in name and hermes_board_id != 6: + pass + elif fmt[0:4] in ('dict', 'list'): + pass + else: + if name[0:6] == 'Hware_': # value comes from the hardware file + value = application.Hardware.GetValue(name) + else: + value = self.GetValue(name, radio_dict) + no_edit = "choice" in fmt or fmt == 'boolean' + if name == 'power_meter_calib_name': + values = self.PowerMeterCalChoices() + txt, cb, btn = self.AddTextComboHelp(col, text, value, values, help_text, no_edit, border=border) + cb.handler = self.OnButtonChangePMcal + self.power_meter_cal_choices = cb + else: + txt, cb, btn = self.AddTextComboHelp(col, text, value, values, help_text, no_edit, border=border) + if name[0:6] == 'Hware_': + cb.handler = application.Hardware.SetValue + else: + cb.handler = self.OnChange + cb.quisk_data_name = name + if col == 1: + col = 4 + border = 0 + else: + col = 1 + border = 2 + self.NextRow() + if hermes_board_id == 6: + if col == 4: + self.NextRow() + help_text = ('This controls the bias level for transistors in the final power amplifier. Enter a level from 0 to 255.' + ' These changes are temporary. Press the "Write" button to write the value to the hardware and make it permanent.') + ## Bias is 0 indexed to match schematic + txt, self.HermesBias0, btn = self.AddTextSpinnerHelp(1, "Power amp bias 0", 0, 0, 255, help_text) + txt, self.HermesBias1, btn = self.AddTextSpinnerHelp(4, "Power amp bias 1", 0, 0, 255, help_text) + enbl = radio_dict["hermes_bias_adjust"] == "True" + self.HermesBias0.Enable(enbl) + self.HermesBias1.Enable(enbl) + self.HermesBias0.Bind(wx.EVT_SPINCTRL, self.OnHermesChangeBias0) + self.HermesBias1.Bind(wx.EVT_SPINCTRL, self.OnHermesChangeBias1) + self.HermesWriteBiasButton = self.AddPushButton(7, "Write", self.OnButtonHermesWriteBias, border=0) + self.HermesWriteBiasButton.Enable(enbl) + self.FitInside() + self.SetScrollRate(1, 1) + def OnHermesChangeBias0(self, event): + value = self.HermesBias0.GetValue() + application.Hardware.ChangeBias0(value) + def OnHermesChangeBias1(self, event): + value = self.HermesBias1.GetValue() + application.Hardware.ChangeBias1(value) + def OnButtonHermesWriteBias(self, event): + value0 = self.HermesBias0.GetValue() + value1 = self.HermesBias1.GetValue() + application.Hardware.WriteBias(value0, value1) + def PowerMeterCalChoices(self): + values = list(conf.power_meter_std_calibrations) # known calibration names from the config file + radio_dict = local_conf.GetRadioDict(self.radio_name) + values += list(radio_dict.get('power_meter_local_calibrations', {})) # local calibrations + values.sort() + values.append('New') + return values + def OnButtonChangePMcal(self, ctrl): + value = ctrl.GetValue() + name = ctrl.quisk_data_name + radio_dict = local_conf.GetRadioDict(self.radio_name) + local_cal = radio_dict.get('power_meter_local_calibrations', {}) + if value == 'New': + if not self.PMcalDialog: + self.PMcalDialog = QPowerMeterCalibration(self, list(local_cal)) + else: + setattr(conf, name, value) + radio_dict[name] = value + local_conf.settings_changed = True + application.Hardware.MakePowerCalibration() + def ChangePMcalFinished(self, name, table): + self.PMcalDialog = None + radio_dict = local_conf.GetRadioDict(self.radio_name) + local_cal = radio_dict.get('power_meter_local_calibrations', {}) + if name is None: # Cancel + name = conf.power_meter_calib_name + values = self.PowerMeterCalChoices() + else: + if table is None: # delete name + del local_cal[name] + name = list(conf.power_meter_std_calibrations)[0] # replacement name + else: # new entry + local_cal[name] = table + conf.power_meter_calib_name = name + radio_dict['power_meter_calib_name'] = name + radio_dict['power_meter_local_calibrations'] = local_cal + local_conf.settings_changed = True + values = self.PowerMeterCalChoices() + self.power_meter_cal_choices.SetItems(values) + application.Hardware.MakePowerCalibration() + try: + index = values.index(name) + except: + index = 0 + self.power_meter_cal_choices.SetSelection(index) + +class RadioHardwareSoapySDR(RadioHardwareBase): # The Hardware page in the second-level notebook for the SoapySDR radios + name_text = { +'soapy_gain_mode_rx' : 'Rx gain mode', +'soapy_setAntenna_rx' : 'Rx antenna name', +'soapy_setBandwidth_rx' : 'Rx bandwidth kHz', +'soapy_setSampleRate_rx' : 'Rx sample rate kHz', +'soapy_device' : 'Device name', +'soapy_gain_mode_tx' : 'Tx gain mode', +'soapy_setAntenna_tx' : 'Tx antenna name', +'soapy_setBandwidth_tx' : 'Tx bandwidth kHz', +'soapy_setSampleRate_tx' : 'Tx sample rate kHz', +} + + help_text = { +'soapy_gain_mode_rx' : 'Choose "total" to set the total gain, "detailed" to set multiple gain elements individually, \ +or "automatic" for automatic gain control. The "detailed" or "automatic" may not be available depending on your hardware.', + +'soapy_setAntenna_rx' : 'Choose the antenna to use for receive.', + +'soapy_device' : "SoapySDR provides an interface to various radio hardware. The device name specifies \ +the hardware device. Create a new radio for each hardware you have. Changing the device \ +name requires re-entering all the hardware settings because different hardware has \ +different settings. Also, the hardware device must be turned on when you change the \ +device name so that Quisk can read the available settings.", + +'soapy_gain_mode_tx' : 'Choose "total" to set the total gain, "detailed" to set multiple gain elements individually, \ +or "automatic" for automatic gain control. The "detailed" or "automatic" may not be available depending on your hardware.', + +'soapy_setAntenna_tx' : 'Choose the antenna to use for transmit.', + +} + def __init__(self, parent, radio_name): + RadioHardwareBase.__init__(self, parent, radio_name) + self.no_device = "No device specified" + if soapy: + self.AlwaysMakeControls() + self.MakeSoapyControls() + else: + radio_dict = local_conf.GetRadioDict(self.radio_name) + radio_type = radio_dict['hardware_file_type'] + self.AddTextL(1, "These are the hardware settings for a radio of type %s" % radio_type, self.num_cols-1) + self.NextRow() + self.AddTextL(1, "The shared library from the SoapySDR project is not available.") + self.NextRow() + self.AddTextL(1, "The shared library is not installed or is not compatible (perhaps 32 versus 64 bit versions).") + self.NextRow() + return + #self.MarkCols() + def NextCol(self): + if self.col == 1: + self.col = 4 + self.border = 0 + else: + self.col = 1 + self.border = 2 + self.NextRow() + def MakeSoapyControls(self): + self.gains_rx = [] + self.gains_tx = [] + radio_dict = local_conf.GetRadioDict(self.radio_name) + local_conf.InitSoapyNames(radio_dict) + self.border = 2 + name = 'soapy_device' + device = radio_dict.get(name, self.no_device) + txt, self.edit_soapy_device, btn = self.AddTextEditHelp(1, self.name_text[name], device, self.help_text[name], span1=1, span2=4) + self.AddPushButtonR(7, "Change..", self.OnButtonChangeSoapyDevice, border=0) + self.NextRow() + self.NextRow() + self.col = 1 + if device == self.no_device: + self.FitInside() + return + + if radio_dict.get("soapy_file_version", 0) < soapy_software_version: + text = "Please re-enter the device name. This will read additional parameters from the hardware." + self.AddTextL(self.col, text, span=6) + self.FitInside() + return + + # Receive parameters + name = 'soapy_setSampleRate_rx' + help_text = 'Available sample rates: ' + rates = ['48', '50', '240', '250', '960', '1000'] + for dmin, dmax, dstep in radio_dict.get('soapy_getSampleRateRange_rx', ()): + tmin = FormatKhz(dmin * 1E-3) + if tmin not in rates: + rates.append(tmin) + if abs(dmin - dmax) < 0.5: + help_text = help_text + '%s; ' % tmin + elif dstep < 0.5: + help_text = help_text + '%s to %s; ' % (tmin, FormatKhz(dmax * 1E-3)) + else: + help_text = help_text + '%s to %s by %s; ' % (tmin, FormatKhz(dmax * 1E-3), FormatKhz(dstep * 1E-3)) + help_text = help_text[0:-2] + '.' + if rates: + rates.sort(key=SortKey) + rate = radio_dict.get(name, '') + txt, cb, btn = self.AddTextComboHelp(self.col, self.name_text[name], rate, rates, help_text, False, border=self.border) + cb.handler = self.OnChange + cb.quisk_data_name = name + self.NextCol() + + len_gain_names = len(radio_dict.get('soapy_listGainsValues_rx', ())) + name = 'soapy_gain_mode_rx' + gain_mode = radio_dict[name] + choices = ['total'] + if len_gain_names >= 3: + choices.append('detailed') + if radio_dict.get('soapy_hasGainMode_rx', 0): + choices.append('automatic') + if gain_mode not in choices: + gain_mode = radio_dict[name] = 'total' + local_conf.settings_changed = True + txt, cb, btn = self.AddTextComboHelp(self.col, self.name_text[name], gain_mode, choices, self.help_text[name], True, border=self.border) + cb.handler = self.OnChange + cb.quisk_data_name = name + self.NextCol() + + name = 'soapy_gain_values_rx' + values = radio_dict[name] + for name2, dmin, dmax, dstep in radio_dict.get('soapy_listGainsValues_rx', ()): + if dstep < 1E-4: + dstep = 0.5 + text = "Rx gain %s" % name2 + help_text = 'Rf gain min %f, max %f, step %f' % (dmin, dmax, dstep) + value = values.get(name2, '0') + value = float(value) + txt, spn, btn = self.AddTextDblSpinnerHelp(self.col, text, value, dmin, dmax, dstep, help_text, border=self.border) + spn.quisk_data_name = name + spn.quisk_data_name2 = name2 + spn.Bind(wx.EVT_SPINCTRLDOUBLE, self.OnGain) + self.gains_rx.append(spn) + self.NextCol() + if len_gain_names < 3: # for 1 or 2 names, just show total gain item + break + self.FixGainButtons('soapy_gain_mode_rx') + + name = 'soapy_setAntenna_rx' + antenna = radio_dict[name] + antennas = radio_dict.get('soapy_listAntennas_rx', ()) + if antenna not in antennas: + if antennas: + antenna = antennas[0] + else: + antenna = '' + radio_dict[name] = antenna + local_conf.settings_changed = True + if antennas: + txt, cb, btn = self.AddTextComboHelp(self.col, self.name_text[name], antenna, antennas, self.help_text[name], True, border=self.border) + cb.handler = self.OnChange + cb.quisk_data_name = name + self.NextCol() + + name = 'soapy_setBandwidth_rx' + help_text = 'Available bandwidth: ' + bandwidths = [] + for dmin, dmax, dstep in radio_dict.get('soapy_getBandwidthRange_rx', ()): + tmin = FormatKhz(dmin * 1E-3) + bandwidths.append(tmin) + if abs(dmin - dmax) < 0.5: + help_text = help_text + '%s; ' % tmin + elif dstep < 0.5: + help_text = help_text + '%s to %s; ' % (tmin, FormatKhz(dmax * 1E-3)) + else: + help_text = help_text + '%s to %s by %s; ' % (tmin, FormatKhz(dmax * 1E-3), FormatKhz(dstep * 1E-3)) + help_text = help_text[0:-2] + '.' + if bandwidths: + bandwidth = radio_dict.get(name, '') + txt, cb, btn = self.AddTextComboHelp(self.col, self.name_text[name], bandwidth, bandwidths, help_text, False, border=self.border) + cb.handler = self.OnChange + cb.quisk_data_name = name + self.NextCol() + + # Transmit parameters + if self.col != 1: + self.NextCol() + name = 'soapy_enable_tx' + enable = radio_dict.get(name, 'Disable') + help_text = 'This will enable or disable the transmit function. If changed, you must restart Quisk.' + txt, cb, btn = self.AddTextComboHelp(self.col, 'Tx enable', enable, ['Enable', 'Disable'], help_text, True, border=self.border) + cb.handler = self.OnChange + cb.quisk_data_name = name + self.NextCol() + + name = 'soapy_setSampleRate_tx' + help_text = 'Available sample rates: ' + rates = [] + for dmin, dmax, dstep in radio_dict.get('soapy_getSampleRateRange_tx', ()): + tmin = FormatKhz(dmin * 1E-3) + rates.append(tmin) + if abs(dmin - dmax) < 0.5: + help_text = help_text + '%s; ' % tmin + elif dstep < 0.5: + help_text = help_text + '%s to %s; ' % (tmin, FormatKhz(dmax * 1E-3)) + else: + help_text = help_text + '%s to %s by %s; ' % (tmin, FormatKhz(dmax * 1E-3), FormatKhz(dstep * 1E-3)) + help_text = help_text[0:-2] + '.' + if rates: + rate = radio_dict.get(name, '') + rates = ('48', '50', '96', '100', '192') + txt, cb, btn = self.AddTextComboHelp(self.col, self.name_text[name], rate, rates, help_text, True, border=self.border) + cb.handler = self.OnChange + cb.quisk_data_name = name + self.NextCol() + + len_gain_names = len(radio_dict.get('soapy_listGainsValues_tx', ())) + name = 'soapy_gain_mode_tx' + gain_mode = radio_dict[name] + choices = ['total'] + if len_gain_names >= 3: + choices.append('detailed') + if radio_dict.get('soapy_hasGainMode_tx', 0): + choices.append('automatic') + if gain_mode not in choices: + gain_mode = radio_dict[name] = 'total' + local_conf.settings_changed = True + txt, cb, btn = self.AddTextComboHelp(self.col, self.name_text[name], gain_mode, choices, self.help_text[name], True, border=self.border) + cb.handler = self.OnChange + cb.quisk_data_name = name + self.NextCol() + + name = 'soapy_gain_values_tx' + values = radio_dict[name] + for name2, dmin, dmax, dstep in radio_dict.get('soapy_listGainsValues_tx', ()): + if dstep < 1E-4: + dstep = 0.5 + text = "Tx gain %s" % name2 + help_text = 'Rf gain min %f, max %f, step %f' % (dmin, dmax, dstep) + value = values.get(name2, '0') + value = float(value) + txt, spn, btn = self.AddTextDblSpinnerHelp(self.col, text, value, dmin, dmax, dstep, help_text, border=self.border) + spn.quisk_data_name = name + spn.quisk_data_name2 = name2 + spn.Bind(wx.EVT_SPINCTRLDOUBLE, self.OnGain) + self.gains_tx.append(spn) + self.NextCol() + if len_gain_names < 3: # for 1 or 2 names, just show total gain item + break + self.FixGainButtons('soapy_gain_mode_tx') + + name = 'soapy_setAntenna_tx' + antenna = radio_dict[name] + antennas = radio_dict.get('soapy_listAntennas_tx', ()) + if antenna not in antennas: + if antennas: + antenna = antennas[0] + else: + antenna = '' + radio_dict[name] = antenna + local_conf.settings_changed = True + if antennas: + txt, cb, btn = self.AddTextComboHelp(self.col, self.name_text[name], antenna, antennas, self.help_text[name], True, border=self.border) + cb.handler = self.OnChange + cb.quisk_data_name = name + self.NextCol() + + name = 'soapy_setBandwidth_tx' + help_text = 'Available bandwidths: ' + bandwidths = [] + for dmin, dmax, dstep in radio_dict.get('soapy_getBandwidthRange_tx', ()): + tmin = FormatKhz(dmin * 1E-3) + bandwidths.append(tmin) + if abs(dmin - dmax) < 0.5: + help_text = help_text + '%s; ' % tmin + elif dstep < 0.5: + help_text = help_text + '%s to %s; ' % (tmin, FormatKhz(dmax * 1E-3)) + else: + help_text = help_text + '%s to %s by %s; ' % (tmin, FormatKhz(dmax * 1E-3), FormatKhz(dstep * 1E-3)) + help_text = help_text[0:-2] + '.' + if bandwidths: + bandwidth = radio_dict.get(name, '') + txt, cb, btn = self.AddTextComboHelp(self.col, self.name_text[name], bandwidth, bandwidths, help_text, False, border=self.border) + cb.handler = self.OnChange + cb.quisk_data_name = name + self.NextCol() + + self.FitInside() + def FixGainButtons(self, name): + radio_dict = local_conf.GetRadioDict(self.radio_name) + gain_mode = radio_dict[name] + if name[-3:] == '_tx': + controls = self.gains_tx + else: + controls = self.gains_rx + for i in range(len(controls)): + ctrl = controls[i] + if gain_mode == "automatic": + ctrl.Enable(False) + elif gain_mode == "total": + if i == 0: + ctrl.Enable(True) + else: + ctrl.Enable(False) + else: # gain_mode is "detailed" + if i == 0: + ctrl.Enable(False) + else: + ctrl.Enable(True) + def OnButtonChangeSoapyDevice(self, event): + if not soapy: + txt = "Soapy shared library (DLL) is not available." + msg = wx.MessageDialog(None, txt, 'SoapySDR Error', wx.OK|wx.ICON_ERROR) + msg.ShowModal() + msg.Destroy() + return + try: + choices = self.GetSoapyDevices() + except: + #traceback.print_exc() + choices = [] + if not choices: + choices = ['No devices were found.'] + device = self.edit_soapy_device.GetValue() + width = application.main_frame.GetSize().width + width = width * 50 // 100 + parent = self.edit_soapy_device.GetParent() + dlg = ListEditDialog(parent, "Change Soapy Device", device, choices, width) + ok = dlg.ShowModal() + if ok != wx.ID_OK: + dlg.Destroy() + return + device = dlg.GetValue() + dlg.Destroy() + if device == self.no_device: + return + if Settings[1] == self.radio_name: + txt = "Changing the active radio requires a shutdown and restart. Proceed?" + msg = wx.MessageDialog(None, txt, 'SoapySDR Change to Active Radio', wx.OK|wx.CANCEL|wx.ICON_INFORMATION) + ok = msg.ShowModal() + msg.Destroy() + if ok == wx.ID_OK: + soapy.close_device(1) + else: + return + txt = soapy.open_device(device, 0, 0) + if txt[0:8] == 'Capture ': + radio_dict = local_conf.GetRadioDict(self.radio_name) + radio_dict['soapy_device'] = device + radio_dict['soapy_file_version'] = soapy_software_version + self.edit_soapy_device.ChangeValue(device) + # Record the new SoapySDR parameters for the new device. Do not change the old data values yet. + for name in ('soapy_listAntennas_rx', 'soapy_hasGainMode_rx', 'soapy_listGainsValues_rx', + 'soapy_listAntennas_tx', 'soapy_hasGainMode_tx', 'soapy_listGainsValues_tx', + 'soapy_getFullDuplex_rx', 'soapy_getSampleRateRange_rx', 'soapy_getSampleRateRange_tx', + 'soapy_getBandwidthRange_rx', 'soapy_getBandwidthRange_tx', + ): + radio_dict[name] = soapy.get_parameter(name, 0) + soapy.close_device(0) + local_conf.settings_changed = True + # Clear our sizer and re-create all the controls + self.gbs.Clear(True) + self.gbs.Add((self.charx, self.charx), (0, 0)) + self.row = 1 + RadioHardwareBase.AlwaysMakeControls(self) + self.MakeSoapyControls() + txt = "Please check the settings for the new hardware device." + msg = wx.MessageDialog(None, txt, 'SoapySDR Change to Radio', wx.OK|wx.ICON_INFORMATION) + msg.ShowModal() + msg.Destroy() + else: + msg = wx.MessageDialog(None, txt, 'SoapySDR Device Error', wx.OK|wx.ICON_ERROR) + msg.ShowModal() + msg.Destroy() + def GetSoapyDevices(self): + choices = [] + for dct in soapy.get_device_list(): + text = '' + try: + driver = dct["driver"] + except: + pass + else: + text = 'driver=%s' % driver + try: + label = dct["label"] + except: + pass + else: + text = text + ', label=%s' % label + choices.append(text) + return choices + def OnChange(self, ctrl): + name = ctrl.quisk_data_name + value = ctrl.GetValue() + radio_dict = local_conf.GetRadioDict(self.radio_name) + radio_dict[name] = value + local_conf.settings_changed = True + # Immediate changes + if name in ('soapy_gain_mode_rx', 'soapy_gain_mode_tx'): + self.FixGainButtons(name) + if soapy and self.radio_name == Settings[1]: # changed for current radio + application.Hardware.ImmediateChange(name, value) + def OnGain(self, event): + radio_dict = local_conf.GetRadioDict(self.radio_name) + obj = event.GetEventObject() + value = obj.GetValue() + name = obj.quisk_data_name + radio_dict[name][obj.quisk_data_name2] = value + local_conf.settings_changed = True + if soapy and self.radio_name == Settings[1]: # changed for current radio + application.Hardware.ChangeGain(name[-3:]) + +class RadioSound(BaseWindow): # The Sound page in the second-level notebook for each radio + """Configure the available sound devices.""" + sound_names = ( # same order as label_help + ('playback_rate', '', '', '', 'name_of_sound_play'), + ('mic_sample_rate', 'mic_channel_I', 'mic_channel_Q', '', 'microphone_name'), + ('sample_rate', 'channel_i', 'channel_q', 'channel_delay', 'name_of_sound_capt'), + ('mic_playback_rate', 'mic_play_chan_I', 'mic_play_chan_Q', 'tx_channel_delay', 'name_of_mic_play'), + ('', '', '', '', 'sample_playback_name'), + ('', '', '', '', 'digital_input_name'), + ('', '', '', '', 'digital_output_name'), + ('', '', '', '', 'digital_rx1_name'), + ('', '', '', '', 'digital_rx2_name'), + ('', '', '', '', 'digital_rx3_name'), + ('', '', '', '', 'digital_rx4_name'), + ('', '', '', '', 'digital_rx5_name'), + ('', '', '', '', 'digital_rx6_name'), + ('', '', '', '', 'digital_rx7_name'), + ('', '', '', '', 'digital_rx8_name'), + ('', '', '', '', 'digital_rx9_name'), + ) + label_help = ( # Same order as sound_names + (1, "Radio Sound Output", "This is the radio sound going to the headphones or speakers."), + (0, "Microphone Input", "This is the monophonic microphone source. Set the channel if the source is stereo."), + (0, "I/Q Rx Sample Input", "This is the sample source if it comes from a sound device, such as a SoftRock."), + (1, "I/Q Tx Sample Output", "This is the transmit sample audio sent to a SoftRock."), + (1, "Raw Digital Output", "This sends the received I/Q data to another program as stereo."), + (0, "Digital Tx0 Input", "This is the transmit audio coming from a digital mode program."), + (1, "Digital Rx0 Output", "This is the main receiver Rx0 audio going to a digital mode program."), + (1, "Digital Rx1 Output", "This is the sub-receiver 1 audio going to a digital mode program."), + (1, "Digital Rx2 Output", "This is the sub-receiver 2 audio going to a digital mode program."), + (1, "Digital Rx3 Output", "This is the sub-receiver 3 audio going to a digital mode program."), + (1, "Digital Rx4 Output", "This is the sub-receiver 4 audio going to a digital mode program."), + (1, "Digital Rx5 Output", "This is the sub-receiver 5 audio going to a digital mode program."), + (1, "Digital Rx6 Output", "This is the sub-receiver 6 audio going to a digital mode program."), + (1, "Digital Rx7 Output", "This is the sub-receiver 7 audio going to a digital mode program."), + (1, "Digital Rx8 Output", "This is the sub-receiver 8 audio going to a digital mode program."), + (1, "Digital Rx9 Output", "This is the sub-receiver 9 audio going to a digital mode program."), + ) + def __init__(self, parent, radio_name): + BaseWindow.__init__(self, parent) + self.radio_name = radio_name + self.MakeControls() + def MakeControls(self): + self.radio_dict = local_conf.GetRadioDict(self.radio_name) + self.num_cols = 8 + for name, text, fmt, help_text, values in local_conf.GetSectionData('Sound'): + if name == 'digital_output_level': + value = self.GetValue(name, self.radio_dict) + no_edit = "choice" in fmt or fmt == 'boolean' + txt, cb, btn = self.AddTextComboHelp(1, text, value, values, help_text, no_edit) + cb.handler = self.OnChange + cb.quisk_data_name = name + break + self.NextRow() + # Add the grid for the sound settings + sizer = wx.GridBagSizer(2, 2) + sizer.SetEmptyCellSize((self.charx, self.charx)) + self.gbs.Add(sizer, (self.row, 0), span=(1, self.num_cols)) + gbs = self.gbs + self.gbs = sizer + self.row = 1 + help_chan = "For the usual stereo device enter 0 for the I channel and 1 for the Q channel. Reversing the 0 and 1 switches the left\ + and right channels. For a monophonic device, enter 0 for both I and Q. If you have more channels enter the channel number. Channels\ + are numbered from zero, so a four channel device has channels 0, 1, 2 and 3." + self.AddTextC(1, "Stream") + self.AddTextCHelp(2, "Rate", +"This is the sample rate for the device in Hertz." "Some devices have fixed rates that can not be changed.") + self.AddTextCHelp(3, "Ch I", "This is the in-phase channel for devices with I/Q data. " + help_chan) + self.AddTextCHelp(4, "Ch Q", "This is the quadrature channel for devices with I/Q data. " + help_chan) + self.AddTextCHelp(5, "Delay", "Some older devices have a one sample channel delay between channels. " +"This must be corrected for devices with I/Q data. Enter the channel number to delay; either the I or Q channel number. " +"For no delay, leave this blank.") + self.AddTextCHelp(6, "Sound Device", "This is the name of the sound device. For Windows, this is the Wasapi name. " +"For Linux you can use the Alsa device, the PortAudio device or the PulseAudio device. " +"The Alsa device are recommended because they have lower latency. See the documentation for more information.") + self.NextRow() + choices = (("48000", "96000", "192000"), ("0", "1"), ("0", "1"), (" ", "0", "1")) + r = 0 + if "SoftRock" in self.radio_dict['hardware_file_type']: # Samples come from sound card + softrock = True + else: + softrock = False + last_row = 8 + for is_output, label, helptxt in self.label_help: + self.AddTextLHelp(1, label, helptxt) + # Add col 0 + value = self.ItemValue(r, 0) + if value is None: + value = '' + data_name = self.sound_names[r][0] + if r == 0: + cb = self.AddComboCtrl(2, value, choices=("48000", "96000", "192000"), right=True) + if r == 1: + cb = self.AddComboCtrl(2, value, choices=("48000", "8000"), right=True, no_edit=True) + if softrock: + if r == 2: + cb = self.AddComboCtrl(2, value, choices=("48000", "96000", "192000"), right=True) + if r == 3: + cb = self.AddComboCtrl(2, value, choices=("48000", "96000", "192000"), right=True) + else: + if r == 2: + cb = self.AddComboCtrl(2, '', choices=("",), right=True) + cb.Enable(False) + if r == 3: + cb = self.AddComboCtrl(2, '', choices=("",), right=True) + cb.Enable(False) + if r >= 4: + cb = self.AddComboCtrl(2, "48000", choices=("48000",), right=True, no_edit=True) + cb.Enable(False) + cb.handler = self.OnChange + cb.quisk_data_name = data_name + # Add col 1, 2, 3 + for col in range(1, 4): + value = self.ItemValue(r, col) + data_name = self.sound_names[r][col] + if value is None: + cb = self.AddComboCtrl(col + 2, ' ', choices=[], right=True) + cb.Enable(False) + else: + cb = self.AddComboCtrl(col + 2, value, choices=choices[col], right=True) + cb.handler = self.OnChange + cb.quisk_data_name = self.sound_names[r][col] + # Add col 4 + if not softrock and r in (2, 3): + cb = self.AddComboCtrl(6, self.ItemValue(r, 4), choices=['']) + elif is_output: + if label == "Digital Rx0 Output" and sys.platform != 'win32': + play_names = application.dev_play[:] + play_names += ["pulse: Use name QuiskDigitalOutput.monitor"] + else: + play_names = application.dev_play + cb = self.AddComboCtrl(6, self.ItemValue(r, 4), choices=play_names) + else: + if label == "Digital Tx0 Input" and sys.platform != 'win32': + capt_names = application.dev_capt[:] + capt_names += ["pulse: Use name QuiskDigitalInput"] + else: + capt_names = application.dev_capt + cb = self.AddComboCtrl(6, self.ItemValue(r, 4), choices=capt_names) + cb.handler = self.OnChange + cb.quisk_data_name = platform_accept + self.sound_names[r][4] + self.NextRow() + r += 1 + if r >= last_row: + break + self.gbs = gbs + self.FitInside() + self.SetScrollRate(1, 1) + def ItemValue(self, row, col): + data_name = self.sound_names[row][col] + if col == 4: # Device names + data_name = platform_accept + data_name + value = self.GetValue(data_name, self.radio_dict) + return value + elif data_name: + value = self.GetValue(data_name, self.radio_dict) + if col == 3: # Delay + if value == "-1": + value = '' + return value + return None + def OnChange(self, ctrl): + data_name = ctrl.quisk_data_name + value = ctrl.GetValue() + #index = ctrl.ctrl.lbox.GetSelections()[0] + #print (value, index, ctrl.choices[index]) + if data_name in ('channel_delay', 'tx_channel_delay'): + value = value.strip() + if not value: + value = "-1" + self.OnChange2(ctrl, value) + +class RadioBands(BaseWindow): # The Bands page in the second-level notebook for each radio + def __init__(self, parent, radio_name): + BaseWindow.__init__(self, parent) + self.radio_name = radio_name + self.parent = parent + self.MakeControls() + def MakeControls(self): + radio_dict = local_conf.GetRadioDict(self.radio_name) + radio_type = radio_dict['hardware_file_type'] + self.num_cols = 8 + #self.MarkCols() + self.NextRow() + self.AddTextCHelp(1, "Bands", +"This is a list of the bands that Quisk understands. A check mark means that the band button is displayed. A maximum of " +"14 bands may be displayed.") + self.AddTextCHelp(2, " Start MHz", +"This is the start of the band in megahertz.") + self.AddTextCHelp(3, " End MHz", +"This is the end of the band in megahertz.") + heading_row = self.row + self.NextRow() + band_labels = radio_dict['bandLabels'][:] + for i in range(len(band_labels)): + if isinstance(band_labels[i], (list, tuple)): + band_labels[i] = band_labels[i][0] + band_edge = radio_dict['BandEdge'] + # band_list is a list of all known bands + band_list = conf.BandList[:] + band_list.append('Time') + if local_conf.ReceiverHasName(radio_type, 'tx_level'): # Must show and edit tx_level + if 'tx_level' in radio_dict: + tx_level = radio_dict['tx_level'] + else: + tx_level = {} + radio_dict['tx_level'] = {} + local_conf.settings_changed = True + else: + tx_level = None + try: + transverter_offset = radio_dict['bandTransverterOffset'] + except: + transverter_offset = {} + radio_dict['bandTransverterOffset'] = transverter_offset # Make sure the dictionary is in radio_dict + try: + hiqsdr_bus = radio_dict['HiQSDR_BandDict'] + except: + hiqsdr_bus = None + try: + hermes_bus = radio_dict['Hermes_BandDict'] + except: + hermes_bus = None + self.band_checks = [] + # Add the Audio band. This must be first to allow for column labels. + cb = self.AddCheckBox(1, 'Audio', self.OnChangeBands) + self.band_checks.append(cb) + if 'Audio' in band_labels: + cb.SetValue(True) + self.NextRow() + start_row = self.row + # Add check box, start, end + for band in band_list: + cb = self.AddCheckBox(1, band, self.OnChangeBands) + self.band_checks.append(cb) + if band in band_labels: + cb.SetValue(True) + try: + start, end = band_edge[band] + start = "%.3f" % (start * 1E-6) + end = "%.3f" % (end * 1E-6) + except: + try: + start, end = local_conf.originalBandEdge[band] + start = "%.3f" % (start * 1E-6) + end = "%.3f" % (end * 1E-6) + except: + start = '' + end = '' + cb = self.AddComboCtrl(2, start, choices=(start, ), right=True) + cb.handler = self.OnChangeBandStart + cb.quisk_band = band + cb = self.AddComboCtrl(3, end, choices=(end, ), right=True) + cb.handler = self.OnChangeBandEnd + cb.quisk_band = band + self.NextRow() + col = 3 + # Add tx_level + if tx_level is not None: + col += 1 + self.row = heading_row + text, help_text = local_conf.GetReceiverItemTH(radio_type, 'tx_level') + self.AddTextCHelp(col, " %s" % text, help_text) + self.row = start_row + for band in band_list: + try: + level = tx_level[band] + level = str(level) + except: + try: + level = tx_level[None] + tx_level[band] = level # Fill in tx_level for each band + level = str(level) + except: + tx_level[band] = 70 + level = '70' + cb = self.AddComboCtrl(col, level, choices=(level, ), right=True) + cb.handler = self.OnChangeDict + cb.quisk_data_name = 'tx_level' + cb.quisk_band = band + self.NextRow() + # Add transverter offset + if isinstance(transverter_offset, dict): + col += 1 + self.row = heading_row + self.AddTextCHelp(col, " Transverter Offset", +"If you use a transverter, you need to tune your hardware to a frequency lower than\ + the frequency displayed by Quisk. For example, if you have a 2 meter transverter,\ + you may need to tune your hardware from 28 to 30 MHz to receive 144 to 146 MHz.\ + Enter the transverter offset in Hertz. For this to work, your\ + hardware must support it. Currently, the HiQSDR, SDR-IQ and SoftRock are supported.") + self.row = start_row + for band in band_list: + try: + offset = transverter_offset[band] + except: + offset = '' + else: + offset = str(offset) + cb = self.AddComboCtrl(col, offset, choices=(offset, ), right=True) + cb.handler = self.OnChangeDictBlank + cb.quisk_data_name = 'bandTransverterOffset' + cb.quisk_band = band + self.NextRow() + # Add hiqsdr_bus + if hiqsdr_bus is not None: + bus_text = 'The IO bus is used to select filters for each band. Refer to the documentation for your filter board to see what number to enter.' + col += 1 + self.row = heading_row + self.AddTextCHelp(col, " IO Bus", bus_text) + self.row = start_row + for band in band_list: + try: + bus = hiqsdr_bus[band] + except: + bus = '' + bus_choice = ('11', ) + else: + bus = str(bus) + bus_choice = (bus, ) + cb = self.AddComboCtrl(col, bus, bus_choice, right=True) + cb.handler = self.OnChangeDict + cb.quisk_data_name = 'HiQSDR_BandDict' + cb.quisk_band = band + self.NextRow() + # Add hermes_bus + if hermes_bus is not None: + rx_bus_text = 'The IO bus is used to select filters for each band. Check the bit for a "1", and uncheck the bit for a "0".\ + Bits are shown in binary number order. For example, decimal 9 is 0b1001, so check bits 3 and 0.\ + Changes are immediate (no need to restart).\ + Refer to the documentation for your filter board to see which bits to set. For the Hermes Lite 2 N2ADR filter set:\n\n\ +160: 0000001\n\ +80: 1000010\n\ +60: 1000100\n\ +40: 1000100\n\ +30: 1001000\n\ +20: 1001000\n\ +17: 1010000\n\ +15: 1010000\n\ +12: 1100000\n\ +10: 1100000\n\ +\n\ +If multiple receivers are in use, the Rx filter will be that of the highest frequency band.\ + The Rx bits are sent as the "Alex" filters (Protocol 1, address 9) and are sent to the J16 interface.' + tx_bus_text = 'The Rx bits are used for both receive and transmit unless the "Enable" box is checked.\ + Then you can specify different filters for Rx and Tx.\n\n\ +The Tx bits are sent as the "Alex" filters and are sent to the J16 interface.' + col += 1 + self.row = heading_row + self.AddTextCHelp(col, " Rx IO Bus", rx_bus_text) + self.AddTextCHelp(col + 1, " Tx IO Bus", tx_bus_text) + self.row += 1 + self.AddTextC(col, "6...Bits...0") + btn = self.AddCheckBox(col + 1, " Enable", self.ChangeIOTxEnable, flag=wx.ALIGN_CENTER|wx.ALIGN_CENTER_VERTICAL) + value = self.GetValue("Hermes_BandDictEnTx", radio_dict) + value = value == 'True' + btn.SetValue(value) + self.row = start_row + try: + hermes_tx_bus = radio_dict['Hermes_BandDictTx'] + except: + hermes_tx_bus = {} + for band in band_list: + try: + bus = int(hermes_bus[band]) + except: + bus = 0 + self.AddBitField(col, 7, 'Hermes_BandDict', band, bus, self.ChangeIO) + try: + bus = int(hermes_tx_bus[band]) + except: + bus = 0 + self.AddBitField(col + 1, 7, 'Hermes_BandDictTx', band, bus, self.ChangeIO) + self.NextRow() + self.FitInside() + self.SetScrollRate(1, 1) + def SortCmp(self, item1): + # Numerical conversion of band name to megahertz + try: + if item1[-2:] == 'cm': + item1 = float(item1[0:-2]) * .01 + item1 = 300.0 / item1 + elif item1[-1] == 'k': + item1 = float(item1[0:-1]) * .001 + else: + item1 = float(item1) + item1 = 300.0 / item1 + except: + item1 = 50000.0 + return item1 + def OnChangeBands(self, ctrl): + band_list = [] + count = 0 + for cb in self.band_checks: + if cb.IsChecked(): + band = cb.GetLabel() + count += 1 + if band == '60' and len(conf.freq60) > 1: + band_list.append(('60', ) * len(conf.freq60)) + elif band == 'Time' and len(conf.bandTime) > 1: + band_list.append(('Time', ) * len(conf.bandTime)) + else: + band_list.append(band) + if count > 14: + dlg = wx.MessageDialog(None, + "There are more than the maximum of 14 bands checked. Please remove some checks.", + 'List of Bands', wx.OK|wx.ICON_ERROR) + dlg.ShowModal() + dlg.Destroy() + else: + radio_dict = local_conf.GetRadioDict(self.radio_name) + radio_dict['bandLabels'] = band_list + local_conf.settings_changed = True + def OnChangeBandStart(self, ctrl): + radio_dict = local_conf.GetRadioDict(self.radio_name) + band_edge = radio_dict['BandEdge'] + band = ctrl.quisk_band + start, end = band_edge.get(band, (0, 9999)) + value = ctrl.GetValue() + if self.FormatOK(value, 'numb'): + start = int(float(value) * 1E6 + 0.1) + band_edge[band] = (start, end) + local_conf.settings_changed = True + def OnChangeBandEnd(self, ctrl): + radio_dict = local_conf.GetRadioDict(self.radio_name) + band_edge = radio_dict['BandEdge'] + band = ctrl.quisk_band + start, end = band_edge.get(band, (0, 9999)) + value = ctrl.GetValue() + if self.FormatOK(value, 'numb'): + end = int(float(value) * 1E6 + 0.1) + band_edge[band] = (start, end) + local_conf.settings_changed = True + def OnChangeDict(self, ctrl): + radio_dict = local_conf.GetRadioDict(self.radio_name) + dct = radio_dict[ctrl.quisk_data_name] + band = ctrl.quisk_band + value = ctrl.GetValue() + if self.FormatOK(value, 'inte'): + value = int(value) + dct[band] = value + local_conf.settings_changed = True + if ctrl.quisk_data_name == 'tx_level' and hasattr(application.Hardware, "SetTxLevel"): + application.Hardware.SetTxLevel() + def OnChangeDictBlank(self, ctrl): + radio_dict = local_conf.GetRadioDict(self.radio_name) + dct = radio_dict[ctrl.quisk_data_name] + band = ctrl.quisk_band + value = ctrl.GetValue() + value = value.strip() + if not value: + if band in dct: + del dct[band] + local_conf.settings_changed = True + elif self.FormatOK(value, 'inte'): + value = int(value) + dct[band] = value + local_conf.settings_changed = True + def ChangeIO(self, control): + radio_dict = local_conf.GetRadioDict(self.radio_name) + dct = radio_dict[control.quisk_data_name] + band = control.quisk_band + dct[band] = control.value + local_conf.settings_changed = True + if hasattr(application.Hardware, "ChangeBandFilters"): + application.Hardware.ChangeBandFilters() + def ChangeIOTxEnable(self, event): + name = "Hermes_BandDictEnTx" + radio_dict = local_conf.GetRadioDict(self.radio_name) + if event.IsChecked(): + radio_dict[name] = "True" + setattr(conf, name, True) + else: + radio_dict[name] = "False" + setattr(conf, name, False) + local_conf.settings_changed = True + if hasattr(application.Hardware, "ImmediateChange"): + application.Hardware.ImmediateChange(name) + +class xxRadioFilters(BaseWindow): # The Filters page in the second-level notebook for each radio + def __init__(self, parent, radio_name): + BaseWindow.__init__(self, parent) + self.radio_name = radio_name + self.MakeControls() + def MakeControls(self): + radio_dict = local_conf.GetRadioDict(self.radio_name) + self.num_cols = 8 + self.NextRow() + bus_text = 'These high-pass and low-pass filters are only available for radios that support the Hermes protocol.\ + Enter a frequency range and the control bits for that range. Leave the frequencies blank for unused ranges.\ + Place whole bands within the frequency ranges because filters are only changed when changing bands.\ + Check the bit for a "1", and uncheck the bit for a "0".\ + Bits are shown in binary number order. For example, decimal 9 is 0b1001, so check bits 3 and 0.\ + Changes are immediate (no need to restart).\ + Refer to the documentation for your filter board to see which bits to set.\ + The Rx bits are used for both receive and transmit, unless the "Tx Enable" box is checked.\ + Then you can specify different filters for Rx and Tx.\ + If multiple receivers are in use, the filters will accommodate the highest and lowest frequencies of all receivers.' + self.AddTextCHelp(1, 'Hermes Protocol: Alex High and Low Pass Filters', bus_text, span=self.num_cols) + self.NextRow() + self.AddTextC(1, 'Start MHz') + self.AddTextC(2, 'End MHz') + self.AddTextC(3, "Alex HPF Rx") + btn = self.AddCheckBox(4, "Alex HPF Tx", self.ChangeEnable, flag=wx.ALIGN_CENTER|wx.ALIGN_CENTER_VERTICAL) + btn.quisk_data_name = "AlexHPF_TxEn" + value = self.GetValue("AlexHPF_TxEn", radio_dict) + value = value == 'True' + btn.SetValue(value) + self.AddTextC(5, 'Start MHz') + self.AddTextC(6, 'End MHz') + self.AddTextC(7, "Alex LPF Rx") + btn = self.AddCheckBox(8, "Alex LPF Tx", self.ChangeEnable, flag=wx.ALIGN_CENTER|wx.ALIGN_CENTER_VERTICAL) + btn.quisk_data_name = "AlexLPF_TxEn" + value = self.GetValue("AlexLPF_TxEn", radio_dict) + value = value == 'True' + btn.SetValue(value) + self.NextRow() + hp_filters = self.GetValue("AlexHPF", radio_dict) + lp_filters = self.GetValue("AlexLPF", radio_dict) + row = self.row + for index in range(len(hp_filters)): + f1, f2, rx, tx = hp_filters[index] # f1 and f2 are strings; rx and tx are integers + cb = self.AddTextCtrl(1, f1, self.OnChangeFreq) + cb.quisk_data_name = "AlexHPF" + cb.index = (index, 0) + cb = self.AddTextCtrl(2, f2, self.OnChangeFreq) + cb.quisk_data_name = "AlexHPF" + cb.index = (index, 1) + bf = self.AddBitField(3, 8, 'AlexHPF', None, rx, self.ChangeBits) + bf.index = (index, 2) + bf = self.AddBitField(4, 8, 'AlexHPF', None, tx, self.ChangeBits) + bf.index = (index, 3) + self.NextRow() + index += 1 + self.row = row + for index in range(len(lp_filters)): + f1, f2, rx, tx = lp_filters[index] # f1 and f2 are strings; rx and tx are integers + cb = self.AddTextCtrl(5, f1, self.OnChangeFreq) + cb.quisk_data_name = "AlexLPF" + cb.index = (index, 0) + cb = self.AddTextCtrl(6, f2, self.OnChangeFreq) + cb.quisk_data_name = "AlexLPF" + cb.index = (index, 1) + bf = self.AddBitField(7, 8, 'AlexLPF', None, rx, self.ChangeBits) + bf.index = (index, 2) + bf = self.AddBitField(8, 8, 'AlexLPF', None, tx, self.ChangeBits) + bf.index = (index, 3) + self.NextRow() + index += 1 + self.FitInside() + self.SetScrollRate(1, 1) + def OnChangeFreq(self, event): + freq = event.GetString() + radio_dict = local_conf.GetRadioDict(self.radio_name) + ctrl = event.GetEventObject() + name = ctrl.quisk_data_name + filters = self.GetValue(name, radio_dict) + filters[ctrl.index[0]][ctrl.index[1]] = freq + setattr(conf, name, filters) + radio_dict[name] = filters + local_conf.settings_changed = True + def ChangeBits(self, control): + radio_dict = local_conf.GetRadioDict(self.radio_name) + name = control.quisk_data_name + filters = self.GetValue(name, radio_dict) + filters[control.index[0]][control.index[1]] = control.value + setattr(conf, name, filters) + radio_dict[name] = filters + local_conf.settings_changed = True + def ChangeEnable(self, event): + btn = event.GetEventObject() + name = btn.quisk_data_name + radio_dict = local_conf.GetRadioDict(self.radio_name) + if event.IsChecked(): + radio_dict[name] = "True" + setattr(conf, name, True) + else: + radio_dict[name] = "False" + setattr(conf, name, False) + local_conf.settings_changed = True + +class BandPlanDlg(wx.Dialog, ControlMixin): + # BandPlan in the application and the config file is a list of [integer hertz, color] and the color is None for "End". + # BandPlan in Settings[4] (global settings) is a list of [string freq in MHz, mode]. + # BandPlanColors is always a list of [mode, color] and the mode "End" is not in the list. + # The color is a string "#aa88cc". The mode is a string "DxData", etc. + def __init__(self, parent): + txt = 'Color Bars on the Frequency X-axis Mark the Band Plan' + wx.Dialog.__init__(self, None, -1, txt, size=(-1, 100)) + ControlMixin.__init__(self, parent) + self.Bind(wx.EVT_SHOW, self.OnBug) + bg_color = parent.GetBackgroundColour() + self.SetBackgroundColour(bg_color) + self.parent = parent + self.radio_name = "_Global_" + self.select_index = 0 + if "BandPlanColors" in Settings[4] and "BandPlan" in Settings[4]: + colors = Settings[4]["BandPlanColors"] + else: + Get = conf.__dict__.get # Get colors from the user's config file, if any. + dflt = "#555555" + colors = [ + ['CW, General', Get("CW", dflt)], + ['CW, Extra', Get("eCW", dflt)], + ['Phone, General', Get("Phone", dflt)], + ['Phone, Extra', Get("ePhone", dflt)], + ['AM', Get("AM", dflt)], + ['Data', Get("Data", dflt)], + ['DxData', Get("DxData", dflt)], + ['RTTY', Get("RTTY", dflt)], + ['SSTV', Get("SSTV", dflt)], + ['Packet', Get("Packet", dflt)], + ['Beacons', Get("Beacons", dflt)], + ['Satellite', Get("Satellite",dflt)], + ['Repeater out', Get("Repeater", dflt)], + ['Repeater in', Get("RepInput", dflt)], + ['Simplex', Get("Simplex", dflt)], + ['Rx only', Get("RxOnly", dflt)], + ['Special', Get("Special", dflt)], + ['Other', Get("Other", dflt)] + ] + self.modes = [] + self.color_dict = {None:"End"} + self.mode_dict = {"End":None} + for mode, color in colors: + self.modes.append(mode) + self.color_dict[color] = mode + self.mode_dict[mode] = color + self.modes.append("End") + self.MakeControls() + def OnBug(self, event): # Bug 16088 + self.gbs.Fit(self) + self.Unbind(wx.EVT_SHOW, handler=self.OnBug) + def MakeControls(self): + self.num_cols = 7 + #self.MarkCols() + self.BgColor1 = wx.Colour(0xf8f8f8) + self.BgColor2 = wx.Colour(0xe0e0e0) + charx = self.charx + chary = self.chary + self.gbs.Add (charx, chary // 2, wx.GBPosition(self.row, 6)) # Spacer at the right ends control expansion + start_row = self.row + self.list_ctrl = list_ctrl = wx.ListCtrl(self, size=(charx * 36, application.screen_height * 6 // 10), + style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.BORDER_SIMPLE | wx.LC_NO_HEADER) + list_ctrl.InsertColumn(0, '', width=charx*16) + list_ctrl.InsertColumn(1, '', width=charx*20) + list_ctrl.SetBackgroundColour(self.BgColor1) + list_ctrl.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnSelected) + hlp = '\ +The graph X-axis has colored bars to mark band segments; CW, phone etc. \ +Colors start at each frequency and continue until the next frequency or "End". \ +To add a row, enter the frequency that starts the segment. Then press the Enter key. \ +It is easier to add all the frequencies for a band first and then correct the modes later.' + txt, edt, btn = self.AddTextEditHelp(3, "New frequency MHz", "", hlp, no_edit=False, border=0) + self.Bind(wx.EVT_TEXT_ENTER, self.OnNewFreq, source=edt) + self.NextRow() + hlp = 'This changes the mode on the selected row. \ +The mode begins at the start frequency and ends at the next frequency.' + txt, self.ComboMode, btn = self.AddTextComboHelp(3, "Mode at start frequency", "AM", self.modes, hlp, no_edit=True, border=0) + self.ComboMode.handler = self.OnChangeMode + self.NextRow() + hlp = 'To delete a row, click the row and press this button.' + self.AddTextButtonHelp(3, "Delete frequency", "Delete", self.OnDelete, hlp, border=0) + self.NextRow() + hlp = 'Press "Save" to save your changes. Press "Cancel" to discard your changes.' + self.AddText2ButtonHelp(3, "Save changes", "Save", self.OnSave, "Cancel", self.OnCancel, hlp, border=0) + self.NextRow() + self.gbs.Add (charx, chary, wx.GBPosition(self.row, 3)) # Spacer before color table + self.NextRow() + width = 0 + buttons = [] + colors = [] + for mode in self.modes: + if mode != "End": + colors.append(self.mode_dict[mode]) + btn = QuiskPushbutton(self, self.OnColorButton, mode) + btn.SetColorGray() + buttons.append(btn) + width = max(width, btn.GetSize().Width) + h = self.quisk_height + 2 + for btn in buttons: # Make all buttons the same size + btn.SetSizeHints(width, h, width, h) + h = self.quisk_height + for i in range(0, len(buttons), 2): # Must be an even number + btn1 = buttons[i] + btn2 = buttons[i + 1] + color1 = wx.StaticText(self, -1, '') + color1.SetSizeHints(h, h, -1, h) + color1.SetBackgroundColour(wx.Colour(colors[i])) + btn1.color_ctrl = color1 + color2 = wx.StaticText(self, -1, '') + color2.SetSizeHints(h, h, -1, h) + color2.SetBackgroundColour(wx.Colour(colors[i + 1])) + btn2.color_ctrl = color2 + bsizer = wx.BoxSizer(wx.HORIZONTAL) + bsizer.Add(btn1, proportion=0) + bsizer.Add(color1, proportion=1, border=3, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT) + bsizer.Add(color2, proportion=1, border=3, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT|wx.RIGHT) + bsizer.Add(btn2, proportion=0) + self.gbs.Add(bsizer, (self.row, 3), span=(1, 3), flag=wx.EXPAND) + self.NextRow() + self.NextRow() + self.gbs.Add(list_ctrl, (start_row, 1), span=(self.row, 1)) + self.gbs.Add (charx, chary // 2, wx.GBPosition(start_row + self.row, 1)) # Spacer at the bottom + self.DrawPlan() + self.gbs.Fit(self) + def DrawPlan(self): + index = 0 + sel = 0 + if "BandPlanColors" in Settings[4] and "BandPlan" in Settings[4]: + for freq, mode in Settings[4]["BandPlan"]: + self.list_ctrl.InsertItem(index, freq) + self.list_ctrl.SetItem(index, 1, mode) + if freq == "3.500": + sel = index + index += 1 + else: + for freq, color in conf.BandPlan: + mode = self.color_dict.get(color, "Other") + self.list_ctrl.InsertItem(index, FormatMHz(freq * 1E-6)) + self.list_ctrl.SetItem(index, 1, mode) + if freq == 3500000: + sel = index + index += 1 + self.MakeBackground() + self.list_ctrl.Select(sel) + self.list_ctrl.EnsureVisible(sel) + def OnSelected(self, event): + self.select_index = event.GetIndex() + mode = self.list_ctrl.GetItem(self.select_index, 1).GetText() + j = self.modes.index(mode) + self.ComboMode.SetSelection(j) + def OnChangeMode(self, ctrl): + self.list_ctrl.EnsureVisible(self.select_index) + mode = self.ComboMode.GetValue() + self.list_ctrl.SetItem(self.select_index, 1, mode) + self.MakeBackground() + def MakeBackground(self): + ctrl = self.list_ctrl + color = self.BgColor1 + for index in range(ctrl.GetItemCount()): + mode = self.list_ctrl.GetItem(index, 1).GetText() + self.list_ctrl.SetItemBackgroundColour(index, color) + if mode == "End": + if color is self.BgColor1: + color = self.BgColor2 + else: + color = self.BgColor1 + def OnNewFreq(self, event): + freq = event.GetString() + win = event.GetEventObject() + win.Clear() + try: + freq = float(freq) + hertz = int(freq * 1E6 + 0.1) + except: + return + i1, i2 = self.SearchBandPlan(hertz) + if i1 != i2: # Frequency is not in the list. Add it at i2. + mode = self.ComboMode.GetValue() + self.list_ctrl.InsertItem(i2, FormatMHz(hertz * 1E-6)) + self.list_ctrl.SetItem(i2, 1, mode) + self.MakeBackground() + self.list_ctrl.Select(i2) + self.list_ctrl.EnsureVisible(i2) + def SearchBandPlan(self, hertz): + # Binary search for the bracket frequency + ctrl = self.list_ctrl + i1 = 0 + i2 = length = ctrl.GetItemCount() + index = (i1 + i2) // 2 + for i in range(length): + freq = ctrl.GetItem(index, 0).GetText() + freq = int(float(freq) * 1E6 + 0.1) + diff = freq - hertz + if diff < 0: + i1 = index + elif diff > 0: + i2 = index + else: # equal to an item in the list + return index, index + if i2 - i1 <= 1: + break + index = (i1 + i2) // 2 + return i1, i2 + def OnDelete(self, event): + self.list_ctrl.DeleteItem(self.select_index) + self.MakeBackground() + if self.select_index > 0: + self.select_index -= 1 + self.list_ctrl.Select(self.select_index) + self.list_ctrl.EnsureVisible(self.select_index) + def OnColorButton(self, event): + btn = event.GetEventObject() + mode = btn.GetLabel() + color = wx.Colour(self.mode_dict[mode]) + data = wx.ColourData() + data.SetColour(color) + dlg = wx.ColourDialog(self, data) + dlg.GetColourData().SetChooseFull(True) + if dlg.ShowModal() == wx.ID_OK: + color = dlg.GetColourData() + color = color.GetColour() + btn.color_ctrl.SetBackgroundColour(color) + btn.color_ctrl.Refresh() + color = color.GetAsString(wx.C2S_HTML_SYNTAX) + self.mode_dict[mode] = color + self.color_dict[color] = mode + dlg.Destroy() + def OnSave(self, event): + mode_color = [] + for mode in self.modes: + if mode != "End": + mode_color.append([mode, self.mode_dict[mode]]) + Settings[4]["BandPlanColors"] = mode_color + freq_mode = [] + plan = [] + ctrl = self.list_ctrl + for index in range(ctrl.GetItemCount()): + freq = ctrl.GetItem(index, 0).GetText() + mode = ctrl.GetItem(index, 1).GetText() + freq_mode.append([freq, mode]) + hertz = int(float(freq) * 1E6 + 0.1) + color = self.mode_dict[mode] + plan.append([hertz, color]) + Settings[4]["BandPlan"] = freq_mode + application.BandPlan = plan + local_conf.settings_changed = True + self.EndModal(wx.ID_OK) + def OnCancel(self, event): + self.EndModal(wx.ID_CANCEL) + +class WsjtxDlg(wx.Dialog, ControlMixin): + def __init__(self, parent): + txt = 'Configure WSJT-X' + wx.Dialog.__init__(self, None, -1, txt) + ControlMixin.__init__(self, parent) + self.Bind(wx.EVT_SHOW, self.OnBug) + bg_color = parent.GetBackgroundColour() + self.SetBackgroundColour(bg_color) + self.parent = parent + self.radio_name = "_Global_" + self.MakeControls() + def OnBug(self, event): # Bug 16088 + self.gbs.Fit(self) + self.Unbind(wx.EVT_SHOW, handler=self.OnBug) + def MakeControls(self): + self.num_cols = 10 + #self.MarkCols() + charx = self.charx + chary = self.chary + self.gbs.Add (charx * 3, chary * 1, wx.GBPosition(self.row, 9)) # Spacer at the top right + self.NextRow() + # Path to WSJT-X + path = local_conf.globals.get('path_to_wsjtx', '') + help_text = "Leave blank for the usual installation path to WSJT-X. If WSJT-X is not in the usual place, enter the path." + txt, self.edit_path, btn = self.AddTextEditHelp(1, "Path to Wsjt-x", path, help_text, no_edit=False, span2=5, border=0) + self.Bind(wx.EVT_TEXT_ENTER, self.OnEditPath, source=self.edit_path) + item = self.AddPushButtonR(8, "Change..", self.OnChangePath, border=0) + self.NextRow() + self.NextRow() + # Configuration name option + value = local_conf.globals.get('config_wsjtx', '') + hlp = '\ +This is the "--config" option used to specify a configuration when WSJT-X starts. It is normally left blank.' + txt, edt, btn = self.AddTextEditHelp(1, "Config name option", value, hlp, no_edit=False, border=0) + edt.SetSizeHints(charx * 20, -1, -1, -1) + self.Bind(wx.EVT_TEXT_ENTER, self.OnConfigName, source=edt) + self.gbs.Add (charx * 3, chary // 2, wx.GBPosition(self.row, 4)) # Middle spacer + # Rig name option + value = local_conf.globals.get('rig_name_wsjtx', 'quisk') + hlp = '\ +When WSJT-X starts, it uses a rig name to keep different instances separate. \ +This is the "--rig-name" option, and it defaults to "quisk".' + txt, edt, btn = self.AddTextEditHelp(5, "Rig name option", value, hlp, no_edit=False, border=0) + self.Bind(wx.EVT_TEXT_ENTER, self.OnConfigRigName, source=edt) + self.NextRow() + self.NextRow() + # End of controls + self.gbs.Add (charx, chary * 2, wx.GBPosition(self.row, 1)) # Spacer at the bottom + self.gbs.Fit(self) + def OnConfigName(self, event): + value = event.GetString() + value = value.strip() + local_conf.globals['config_wsjtx'] = value + local_conf.settings_changed = True + def OnConfigRigName(self, event): + value = event.GetString() + value = value.strip() + local_conf.globals['rig_name_wsjtx'] = value + local_conf.settings_changed = True + def OnChangePath(self, event): + path = self.edit_path.GetValue() + path = path.strip() + if not path: + if sys.platform == 'win32': + path = "C:\\WSJT\\wsjtx\\bin\\wsjtx.exe" + else: + path = "/usr/bin/wsjtx" + direc, fname = os.path.split(path) + dlg = wx.FileDialog(application.main_frame, "Path to WSJT-X", direc, fname, "", wx.FD_OPEN|wx.FD_FILE_MUST_EXIST) + if dlg.ShowModal() == wx.ID_OK: + path = dlg.GetPath() + path = path.strip() + self.edit_path.SetValue(path) + local_conf.globals['path_to_wsjtx'] = path + local_conf.settings_changed = True + dlg.Destroy() + def OnEditPath(self, event): + path = self.edit_path.GetValue() + path = path.strip() + local_conf.globals['path_to_wsjtx'] = path + local_conf.settings_changed = True diff --git a/defaults.html b/defaults.html new file mode 100644 index 0000000..635831c --- /dev/null +++ b/defaults.html @@ -0,0 +1,33 @@ + + + +QUISK Configuration Defaults: quisk_conf_defaults.py + + + + + + + + +
+ +
+ + diff --git a/docs.html b/docs.html new file mode 100644 index 0000000..e53ca47 --- /dev/null +++ b/docs.html @@ -0,0 +1,1766 @@ + + + + + + + + +Documentation for Quisk + + + + + + + + + +
+

+ +Welcome to QUISK (December 2023) +

+ +

+This is Quisk, a Software Defined Radio (SDR). You supply an antenna and +a complex (I/Q) mixer to convert the radio spectrum to a low IF. Then send +that IF to your computer using the sound card, Ethernet or USB. +The Quisk software will read the I/Q data, tune it, filter it, +demodulate it, and send the audio to headphones or speakers. +Quisk has a microphone input and a key input so it can operate as a +complete transceiver. Quisk works with this hardware: +

+
+
    +
  • SoftRock connected to the sound card
  • +
  • Many other SDR's connected to the sound card
  • +
  • SDR-IQ connected by USB
  • +
  • Perseus connected by USB
  • +
  • N2ADR hardware connected by Ethernet and IP
  • +
  • HiQSDR hardware connected by Ethernet and IP
  • +
  • The Hermes-Lite project at hermeslite.com
  • +
  • Quisk can be used as a pan adapter, and can control some radios
  • +
+
+ +

+Quisk is small and simple, and has been designed so that it is easy to +change Quisk to suit your own hardware. Quisk rhymes with +"brisk", and is QSK plus a few letters to make it +easier to pronounce. QSK is a Q signal meaning full breakin CW +operation, +and Quisk has been designed for low latency. Quisk includes an input +keying signal that can mute the audio and substitute a sidetone. +
+ +
+Please read the file CHANGELOG.txt +for changes. +
+ +
+When running Quisk for the first time, please press the "Help" +button on the lower right. +
+ +
+

+

News

+

+The newest versions of Linux and Raspberry Pi OS do not allow installing Quisk in the system Python package directory. +I am working on a solution for this. +

+
+

+Python 2 is no longer supported. +Python 2 was obsolete as of January 1, 2020. And the new code by Ben, AC2YD, needs Python 3. It is troublesome to write +code that runs on both Python 2 and Python 3. So it is time to stop supporting Python 2 in Quisk. Please upgrade to +Python 3. If you have both versions, use Python 3 for Quisk. +

+
+

Credits

+

+Quisk was originally written by James Ahlstrom, N2ADR. +
+ +
+Thanks to Leigh L. Klotz, Jr. WA5ZNU for configuration improvements, +factoring +out my eccentric hardware control, and adding panadapter and other +hardware +support. +
+ +
+Thanks to Franco Spinelli for a fix for the H101 hardware. +
+ +
+Thanks to Andrew Nilsson VK6JBL for adding support for SoftRock Rx and +Tx. +
+ +
+Thanks to Terry Fox, WB4JFI, for code to support the Charleston +hardware. +
+ +
+Thanks to Maitland Bottoms, AA4HS, for the sub-module linkage patches. +
+ +
+Thanks to Philip G. Lee for adding native support for PulseAudio. +
+ +
+Thanks to Eric Thornton, KM4DSJ, for adding async support for PulseAudio. +
+ +
+Many others contributed to Quisk, and are mentioned in comments in the source code. +
+
+

+ +

Installation

+

+Quisk is free open source software written in Python and C. It is hosted on the PyPi repository +https://pypi.org. +It can be installed using the standard Python setup tools. See specific directions below. +

+
+

+If you have Python 3 installed on your computer, just continue to use that version. +Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11. +If you want to install a more recent version, uninstall the old version first. +Running multiple versions of Python is possible but can be confusing and is not necessary. +A Quisk installation is needed for each version of Python you have. +

+
+

Windows Initial Installation

+

+Windows does not include Python, so you must first install the most recent version of 64-bit Python 3. +If you already have 64-bit Python 3 installed, just keep using it. +Quisk requires Python 3.8 or 3.9 or 3.10 or 3.11. +There may be newer versions of Python, but until support is added, use the versions listed above. +Python is available from http://www.python.org. +There is an option to Add Python to PATH. You MUST select +this option so that you can start python by just typing "python" instead of the whole path +to your install directory. My install directory is: +
+C:\users\jim\AppData\Local\Programs\Python\Python39 +
+And "AppData" is invisible. To avoid problems, check Add Python to PATH. +If you forget, it is worth it to uninstall and reinstall Python. +Quisk is really designed for curious people who want to play with the code, but Windows +tries to shield users from program details. +

+
+

+Next open Windows PowerShell (not PowerShell ISE). This is on the Start button menu on Windows 10; or use the +search bar. Enter "python --version" to make sure that Python is installed +and what version you have. If "python" is not found, it might not be on your Path. +You will need to keep typing your_install_directory\python instead of "python". +

+
+

+Next upgrade some Python modules to the newest version, then install Quisk. Enter these commands: +
+python -m pip install --upgrade pip +

+python -m pip install --upgrade setuptools +

+python -m pip install --upgrade wxPython +

+python -m pip install --upgrade pyserial +

+python -m pip install --upgrade quisk +

+You should then be able to start Quisk with the command "quisk". +You can also start Quisk with "python -m quisk". +To create a Quisk shortcut on your desktop, right-click an empty space and select "New" and "Shortcut". +Use "quisk.exe" as the command and "Quisk" as the name. +The "quisk.exe" program is in your_install_directory\Scripts. +If you are curious, Quisk and its Python source files are installed in "your_install_directory\Lib\site-packages\quisk". +If you change to that directory you can run Quisk with "python quisk.py". +

+
+

+To get started you must tell Quisk what kind of radio hardware you have. Press the Config button and select Radios. +Then set your sound devices for that radio; the device for the radio speakers, the microphone and so forth. All configuration +is (mostly) from the Config button. Ignore old directions and don't bother with a config file. +

+
+

Windows Quisk Upgrade

+

+To upgrade to a newer version of Quisk, use pip. Remember that if Python is not on your Path, you will +need to type out the whole path. +You should check for newer versions of the other modules twice a year. +
+
+python -m pip install --upgrade quisk +
+
+You can also install an older version of Quisk. You may need to do that if the most recent version fails for some reason. Use: +
+
+python -m pip install quisk==4.1.51 +
+
+

+

Windows Uninstall Quisk

+

+python -m pip uninstall quisk +
+
+

+

Linux Initial Installation

+

+
+The libsoundio-dev module is NO LONGER required! +
+
+Linux runs on a wide variety of computers with different processors and versions of Linux. +Therefore Quisk is compiled for each one. To do this, Python and a number of other packages are required. +Most likely both Python2 and Python3 are already installed on your computer. Either the 32-bit or 64-bit +version will work. On my Ubuntu machine, "python2" starts Python2 and "python3" starts Python3. +Just "python" starts Python2, but this will change as Python2 is phased out. +Python2 is obsolete, so you should only install Quisk to Python3, and use "python3" to start it. +Install these packages. Some of these can be installed from Python using "pip", +but it is better to use your package manager because pip and the package manager do not coordinate. +If newer versions of these packages become available, Linux will notify you. +Use the most recent version of python3-wxgtk available. +

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Package DescriptionPython3 Package
Discrete fourier transformsudo apt-get install libfftw3-dev
Alsa sound librarysudo apt-get install libasound2-dev
Portaudio sound librarysudo apt-get install portaudio19-dev
Pulseaudio sound librarysudo apt-get install libpulse-dev
Python developmentsudo apt-get install python3-dev
Python library developmentsudo apt-get install libpython3-dev
wxPython GUI librarysudo apt-get install python3-wxgtk4.0
Python USBsudo apt-get install python3-usb
Python serial portsudo apt-get install python3-serial
Python installation toolssudo apt-get install python3-setuptools
Pip package installersudo apt-get install python3-pip
+

+

+If you want to use the SoapySDR module make sure that SoapySDR is installed from its Pothosware site before you build Quisk. +Otherwise Quisk will not have the files it needs. You can install SoapySDR later, but then you +will need to reinstall Quisk. This is also true of any other external packages that have their own C source. +
+
+

+

Linux Pip Installation

+

+You can use the Python program "pip" to install Quisk into the Python package directory. This is the easiest +method, but it will not install the source files. If you want the source files see below. +
+
+sudo -H python3 -m pip install --upgrade quisk +
+
+This will install the Python files and all other files except for the C source. This is the easiest method +if you do not want to work in C. +To run Quisk, use this command from a terminal: +
+
+python3 -m quisk +
+
+You can create a panel launcher on your desktop with the command to run Quisk. +
+
+If the installation of Quisk fails, you can force re-installation: +
+
+

+
+sudo -H python3 -m pip uninstall quisk
+sudo -H python3 -m pip install  --no-cache-dir  --force-reinstall  quisk
+
+

+
+You can also install an older version of Quisk. You may need to do that if the most recent version fails for some reason. Use: +
+
+sudo -H python3 -m pip install quisk==4.1.51 +
+
+To get started you must tell Quisk what kind of radio hardware you have. Press the Config button and select Radios. +Then set your sound devices for that radio; the device for the radio speakers, the microphone and so forth. All configuration +is (mostly) from the Config button. Ignore old directions and don't bother with a config file. +
+
+To edit the Quisk Python files you need to know where they are. To find out, import quisk and print it: +
+
+

+
+jim@IntelNUC:~$ python3
+Python 3.6.9 (default, Oct  8 2020, 12:12:24) 
+[GCC 8.4.0] on linux
+Type "help", "copyright", "credits" or "license" for more information.
+--- import quisk
+--- quisk
+module 'quisk' from '/usr/local/lib/python3.6/dist-packages/quisk/__init__.py'
+--- 
+
+
+
+

Linux Pip Upgrade

+

+To upgrade to a newer version of Quisk, use pip: +
+
+sudo python3 -m pip install --upgrade quisk +
+
+

+

Linux Uninstall Quisk

+

+sudo python3 -m pip uninstall quisk +

+
+
+

Linux Source Installation

+

+The previous method using pip is simple, but will not install the C source files. If you want those proceed as follows. +Go to https://pypi.org/project/quisk and under "Navigation" +select "Download files". Download the quisk-x-x-xx.tar.gz file. This is the source file. The ".whl" files are for Windows. +Change directory to where the file was downloaded, probably "Downloads", and uncompress and untar it. +

+cd ~/Downloads +
+tar zxf quisk-x.x.xx.tar.gz +

+Change directories to the Quisk directory: +

+cd quisk-x.x.xx +

+Now compile Quisk with the "make" command: +

+make quisk3 +

+If "make" fails, you probably have missing packages or missing "-dev" packages. Try to figure out what is missing from the error messages. +Now you can run quisk with the command: +

+python3 quisk.py +

+At this point you have a choice of where to install Quisk. You could just rename the quisk-x.x.xx directory +to a more convenient name, like "quisk" in your home directory, and run Quisk from there. +This is convenient if you want to +alter the Quisk code. Or you could install Quisk into the Python system. +

+python2 setup.py build +
+sudo python2 setup.py install +

+

+

Quisk Files

+

+
+These are the Quisk files in the distribution: +
+ +
+

+
    + +
  • quisk.py is the main program and is written in the Python +language. +Python is a powerful but easy to learn language, and I hope you have +fun changing Quisk to make it do what you want. Python is also useful +for general electronics calculations such as complex arithmetic. See +www.python.org. Quisk.py uses the wxPython Python package to make the +screen interface.
  • +
+
    + +
  • help.html is the help file for quisk. Press the "Help" button.
  • +
+
    + +
  • _quisk.so is the _quisk extension module for Linux, and _quisk.pyd is the extension module DLL used by Windows. +
    +
  • +
+
    + +
  • sdriq.so is the extension module needed for the SDR-IQ. It needs +_quisk.so to be available when it starts.
  • +
+
    + +
  • makefile is the makefile, and you must run "make" to create a new +_quisk.so unless you use a Python installer that creates _quisk.so +itself.
  • +
+
    + +
  • setup.py is used by makefile and the Python installers.
  • +
+
    + +
  • quisk_conf_defaults.py is the basic configuration file imported +into +all other configuration files. Read it (but don't change it) to see +what you can change in your own quisk_conf.py.
  • +
+
    + +
  • quisk_conf_*.py are various Quisk configuration files. Copy one +of them +to your own .quisk_conf.py and edit that file. I may publish new model +files in the future, and you don't want your changes to be overwritten. +
  • +
+
    + +
  • quisk_hardware_*.py are various quisk hardware control programs. +If you +have custom hardware, import one of these files into your +quisk_conf.py. Or copy one of them to your own quisk_hardware.py, edit +that file, and import it in .quisk_conf.py.
  • +
+
    + +
  • quisk.c, quisk.h are the files for the _quisk extension module +used by +quisk.py. The other C-language files are linked with these to make +_quisk.so and _quisk.pyd. +
    +
  • +
+
    + +
  • sound.c is the general purpose sound code for all sources.
  • +
+
    + +
  • sound_portaudio.c is the sound card access code for PortAudio. PortAudio is optional.
  • +
+
    + +
  • sound_pulseaudio.c is the sound card access code for PulseAudio.
  • +
+
    + +
  • sound_alsa.c is the sound card access code for the ALSA drivers. +
    +
  • +
+
    + +
  • sound_directx.c is the sound card access code for DirectX.
  • +
+
    + +
  • is_key_down.c is the hardware key checker for the PC. I use +Ethernet to +send the key status, but there is code for the parallel port and dummy +code too.
  • +
+
    + +
  • sdriq.c, sdriq.h are the files that make sdriq.so and support the +SDR-IQ.
  • +
+
    + +
  • microphone.c reads the microphone audio and sends it to your +hardware +using Ethernet. Change it for other sound access.
  • +
+
    +
+
    + +
  • docs.html is Quisk documentation. Look for other *.html and *.txt too.
  • +
+
    + +
  • portaudio.py is a utility program. Run it to list your PortAudio +devices. It is not used by the Quisk program.
  • +
+
+

Configuration

+

+The Quisk "Config" button brings up a number of status and configuration screens. +Quisk supports multiple types of radio hardware and each type has different parameters. +Each block of parameters is called a "radio". It is a named block of settings Quisk uses +to control a specific kind of hardware. +So a single Quisk can have parameters for a SoftRock and an HL2. +You specfy the radio you want when starting Quisk. +
+
+ +When you first install Quisk, you will not have any settings for your radio. +Press the Config button and go to the Radios screen. +Then create a radio by specifying the general hardware type and give it a name of your choosing. +For a Hermes-Lite, specify "Hermes" as the hardware type and call it "HL2" (or some other name). +Press "Add" and a new tab for your radio will appear. Look through the various settings on the HL2 tab. +The parameters for the radios are stored in the file quisk_settings.json. +
+
+ +A special radio called "ConfigFileRadio" is always available. +It takes its parameters from a configuration file. +You can set almost everything with the screens, but you can have a configuration file if you want. +Most users will not need a configuration file. +For Linux, the default configuration file name is ".quisk_conf.py" in your home +directory; that is, "~/.quisk_conf.py". For Windows, the default +configuration file name is quisk_conf.py in your My Documents folder. + + + + +
+
+

+

Sound Cards

+

+If you use a sound card for input, the quality of your sound card is +critical; +but you can start with the sound card you have. Check the Graph screen +with no input to see the noise floor. It should be as flat and as low +as +possible, +with no bump near zero Hertz. The 0dB line at the top of the Graph +screen +is the +maximum level, so if your noise floor is at -90 dB, you have that much +dynamic range. The IF (sound) input to the sound card should raise the +noise +floor only slightly to avoid losing dynamic range. +
+ +
+The sample rate determines how much of the band you can see on the +screen. My 96 kHz card shows a little over 80 kHz of bandwidth, from +-40 kHz to + 40 kHz centered at the VFO frequency. Generally you +would choose the +highest +rate available to get the most visible bandwidth. Be aware that a card +claiming to work at (say) 192 kHz may in fact play at that rate, but +only capture (record) audio at a lower rate. It is the capture rate +that matters. +Enter only the sample rate you know your raw hardware supports for +capture. +
+ +
+If you use the SDR-IQ or other hardware for input, you still need a +sound card for sound output. The quality of this card is not so +important, so try the one you have. Be aware that most sound +cards require the capture and playback rate to be the same when used +for both. Here are some sample configurations: +
+

+
    + +
  • SoftRock Rx/Tx: Receive to card 1, Transmit to card 1 at the same +rate, radio sound to card 2 at 48 kHz, microphone input from card 2 or +3 at 48 kHz.
  • + +
  • SoftRock Rx: Receive to card 1, radio sound to card1 at the same +rate; OR radio sound to card 2 at 48 kHz.
  • + +
  • Other: Receive from SDR-IQ or other hardware, radio sound to card +1 at 48 kHz. Add a microphone to card1 at 48 kHz, or to card2 at +48 kHz.
  • + +
  • Panadapter: There is no radio sound. Enter a null name "" +for the play device. +
    +
  • +
+

+If you buy a new sound card, make sure you know the +capture (recording) sample rates and the noise level. Sound cards +are usually specified over +the audio range up to 24 kHz or so. But we need low noise and +distortion +over the whole range. +

+
+

Linux Names

+

+Quisk can use PulseAudio, PortAudio or ALSA to access your sound card. +Names can be a fragment of text from the device description. It is +better to use this text search rather than an index number, because the +index number can change if you plug and unplug USB sound cards. +
+ +
+The ALSA drivers use different names for the same sound card +to provide different access. The names "hw:0" and "hw:1" refer +to the raw hardware devices of the first and second sound card. +You should use the raw hardware if possible. If the raw devices don't +work, +use the "plughw" name. The ALSA name can also be a string +name. Here are some ALSA names: +
+

+
+
"hw:0"		# First sound card
+"hw:1"		# Second sound card, etc.
+"plughw"	# plug device
+"default"	# alsa default device
+"alsa:NVidia"	# Search for the name in the alsa device description
+
+
+

+Alsa names starting with "alsa:" are an extension to the normal alsa +names. They search for the text after the colon in the alsa +device +name. The alsa device names are shown on the config screen. +Or you +can start a terminal window and enter "aplay -l" for a list of play +devices, or "arecord -l" for a list of capture devices. See alsa_names for +more information. +
+ +
+The PortAudio interface is now optional. Many users are changing to PulseAudio. +You can run "python portaudio.py" in a terminal window to +see a list of available PortAudio names. Here are some PortAudio names: +
+

+
"portaudio:(hw:0,0)"    First sound card.
+"portaudio:(hw:1,0)"    Second sound card, etc.
+"portaudio:NVidia"      Search for the name in the portaudio device description.
+"portaudio#1"           Directly specified index.
+"portaudiodefault"      May give poor performance on capture.
+
+
+

Linux Sound Servers

+

+Newer Linux systems are now shipping with PulseAudio enabled. +PulseAudio is a sound server, a program that takes control of your +sound cards, and controls usage by applications. The idea is that +your applications talk to PulseAudio, and PulseAudio talks to the sound +cards. Another example of a sound server is JACK. +You can control the +sound routing with the pavucontrol program. Remarkably, this is +not included with PulseAudio, and you will need to install the +pavucontrol package first. +
+ +
+Thanks to Philip G. Lee and Eric Thornton, KM4DSJ, Quisk now has native support for PulseAudio. +For PulseAudio devices, use the name "pulse:name" and connect the streams +to your hardware devices using a PulseAudio control program like pavucontrol. The name "pulse" +alone refers to the "default" device. The PulseAudio names are quite long; +for example "alsa_output.pci-0000_00_1b.0.analog-stereo". Look on the screen +Config/Sound to see the device names. There is a description, a PulseAudio name, +and for ALSA devices, the ALSA name. +Instead of the long PulseAudio name, you can enter a substring of any of these three strings. +An example is: + +

+
+
+# As seen on the Config/Sound screen:
+     CM106 Like Sound Device Analog Stereo
+     alsa_output.usb-0d8c_USB_Sound_Device-00-Device.analog-stereo
+     USB Sound Device USB Audio (hw:1,0)
+
+# Use the default pulse device for radio sound:
+   "pulse"
+# Use a PulseAudio name for radio sound:
+   "pulse:alsa_output.usb-0d8c_USB_Sound_Device-00-Device.analog-stereo"
+# Abbreviate the PulseAudio name:
+   "pulse:alsa_output.usb"
+# Another abbreviation:
+   "pulse:CM106"
+
+
+ +

+The PulseAudio code should not cause problems, but I am not sure what happens if PulseAudio is not +installed, or if you replace it with JACK. This config file option will turn off all but directly +entered "pulse:" names: +

+
show_pulse_audio_devices = False
+
+ + +
+ +

Linux Problems

+ +

+If Quisk appears to run but you get no sound input or output, you +may be having trouble +with your settings. Start Quisk and look at the graph. You should get a +moving +line display. Look at the Config screen. Interrupts should be +increasing and latencies +should fluctuate. If all this looks normal, but you get no sound +output, or you get only +white noise output, then you may need to change your settings with a +mixer program. +
+ +
+If you capture data with the sound card (no SDR-IQ) then you need +to set the "capture +device" to the line-in jack, and set the volume of the line-in to 100%. +To play sound, +you need to increase the volume of the playback device. Since a typical +sound card has +ten or twenty controls for all its analog and digital inputs and +outputs, it is a guessing +game to figure out which control to adjust. +
+ +
+Basically you start the alsamixer program (use "man alsamixer" first) +and adjust the volume +controls and capture device until Quisk works. It is wise to reduce or +mute unwanted inputs +to avoid adding extra noise. +Quisk does not do this for you. But once you have the controls set, +they will stay the same +and Quisk should keep working until you run another audio program that +changes them. +
+ +
+To make Quisk adjust the mixer controls when it starts, you need to +know the control id number. +Run the command "amixer -c 0 contents" (for card zero) and look at the +control ids, names +and values of all your controls. Figure out the control you need to +adjust. For a setable +option (on/off) the control value is one or zero. For a volume it is a +number from 0.0 to +1.0. Make a list of (device_name, numid, value) and add it to +mixer_settings in your +.quisk_conf.py file (see quisk_conf_defaults.py). I don't need to do +this on my computer +except for the microphone input on my second sound card. +
+ +
+If you really get stuck, try one of these commands (see the "man" +page): +
+

+
    + +
  • alsamixer An ALSA mixer program with a curses interface.
  • +
+
    + +
  • amixer A character ALSA mixer.
  • +
+
    + +
  • aplay Play sound.
  • +
+
    + +
  • arecord Capture sound.
  • +
+
    + +
  • speaker-test Play sound to test your speakers.
  • +
+

+
+And try to play an audio CD or run some other Linux audio program just +to see that you +have a working sound system. +If you can't get ALSA to work, you could try the PortAudio or PulseAudio interface by +just +changing the sound card names. +
+ +
+

+

Windows Names

+

+To see what sound cards you have, use the Control Panel item Sound +Devices. There is a separate list for capture (recording) and +playback devices, and a specified default device for each. The +name of the default device is "Primary". To specify your sound +card name, use either "Primary" or a substring of the device +name. The search is case sensitive. +
+
+

+

SoftRock

+

+SoftRock radios use an analog mixer to change the RF signal to stereo audio, and then use a sound card to digitize it. +A high quality sound card is advisable. The analog mixer is not perfect, and it will be necessary to adjust the I and Q +signals to equal amplitude and 90 degrees phase difference. Use the Config/Config screen buttons to +bring up an adjustment screen. Adjustments must be made for each band, and separately for transmit and receive. +The adjustment depends on both the VFO frequency and the tuning offset from the VFO. See my paper +http://james.ahlstrom.name/phase_corr.html for more information. +A good strategy is to pick a VFO near the band center, and record corrections at an Rx frequency of -15000, -1000, 1000 and 15000 Hertz. +If the corrections are sensitive to VFO, record the corrections for these same Rx frequencies at VFOs equal to +or slightly outside the upper and lower band edges. +To effectively adjust for multiple VFO frequencies, the VFOs must have the same table of Rx frequencies. +You can add as many correction points as desired. The corrections are saved in the file quisk_init.json. This file can be edited +by hand if you are a Python expert. Otherwise you can just read the values. +
+
+To create a Receive correction point for a given VFO and frequency, attach a signal generator to the SoftRock through an attenuator, +and look at the image on the graph screen. If the signal is 3500 Hertz above the VFO, the image is 3500 Hertz below it. +If you don't have a signal generator, find a strong station on the band and look at its image. Minimize the image by adjusting +the amplitude and phase sliders on the adjustment screen, and then press "Save". +
+
+To create a Transmit correction point for a given VFO and frequency, attach the SoftRock RF output to a spectrum analyzer through an attenuator, +and look at the image. If you don't have a spectrum analyzer use a second receiver tuned to the image. Minimize the image by adjusting +the amplitude and phase sliders on the adjustment screen, and then press "Save". +
+
+

+

SDR-IQ as Input

+

+Quisk can use an SDR-IQ from RfSpace instead of a sound card as input. +Set up a radio of type SdrIQ. The SDR-IQ uses a serial port to connect to Quisk. When you plug it in, +it will create a USB serial port and connect to it. +

+
+

+On Linux the serial port has a name like /dev/ttyUSB0. Look in /dev or use "dmesg | tail" to figure out what port it is using. +Then enter that port as the "Serial port" on the Config/radio/Hardware screen. The serial ports are part of the "dialout" group. +Add yourself to the "dialout" group so you have permission to use the serial port. You also need a serial port USB +driver for the ft245 chip in the SDR-IQ, but Linux generally comes with a suitable driver. +

+
+

+On Windows the "Serial port" name is not used, and Quisk will search for the port in use. +On Windows 10, you should see a device "SDR-IQ" in Device Manager in the "View/Devices by Container" tab. +In earlier versions of Windows, port names are COM1, COM2 etc. and use the "USB Serial Converter" driver. +Windows should find this driver by itself. +

+
+

Perseus as Input

+

+Quisk can use an Perseus HF receiver from Microtelecom instead of a sound card as input. +Set up a radio of type Perseus. The Perseus uses a native USB interface to connect to Quisk. +The Quisk perseuspkg extension relies on libperseus-sdr + open source library to manage Perseus hardware and receive the I/Q samples stream. +

+
+

+Follow the instruction into GitHub repository to compile and install the library. +On Suse distribution the library is available as binary package. +Next compile the perseuspkg using the command: +

+
+
+make perseus3
+
+
+

+The several sample rates can be selected opening Config panel: in +the Config tab there is the Samples rates dropdown. +The input analog filter can be switched in using the button Wideband.
+The input attenuator is operate via the button RF, that allows to select +the four attenuator steps.
+The ADC commands for dithering and preamplifier are found on +left bottom corner as ADC Dither and ADC Preamp.
+

+
+

Timing

+

+There are several configuration parameters devoted to tuning; read the +file quisk_conf_defaults.py for documentation. For most users, Quisk +should run fine with the default settings. But if you use Quisk as part +of a QSK CW transmitter, you should reduce latency_millisecs to as low +a +value as possible. This will reduce latency, but increase the +likelihood of clicks and pops due to sound buffer underruns. +
+ +
+

+

USB Control

+

+Many radio devices are now controlled through a USB interface. In +many cases, the interface is actually a serial port, and an external or +internal USB to serial converter is used. In other cases, the USB +is native, but requires a custom device driver. In still other +cases, the USB device announces itself as a standard device such as a +sound device or human interface device, and uses a standard operating +system built-in driver. +
+ +
+

+

Linux

+

+Default USB permissions do not allow a non-root user to write to the +bus. You may find that Quisk will complain about lack of +permission to access the USB. You could test this by running +Quisk as root and seeing if that works; but this is not acceptable +except for testing. To change USB permissions, add a rule to +/etc/udev/rules.d/local.rules (for SoftRock on Debian and Ubuntu) like +this: +
+ +
+ SUBSYSTEM=="usb", ATTR{idVendor}=="16c0" , +ATTR{idProduct}=="05dc", MODE="0666", GROUP="dialout" +
+ +
+This changes the USB device permissions to read/write for all users, +and changes the group to the "dialout" group. Default group +permissions are read/write, so if you are in the "dialout" group, you +don't need "MODE"; modify as appropriate. To load the new rule, you can either reboot or on Ubuntu use +
+ +
+ sudo udevadm control --reload-rules +
+ +
+

+

Custom Hardware

+

+Quisk comes with hardware files for many types of radio. See the various quisk_hardware_*.py files. +But if you have a radio that Quisk does not support, or if you want to customize an included hardware file, you +can write your own hardware file and enter the name on the Config/radio/Hardware screen. +A model hardware file is included as quisk_hardware_model.py. +It is useful as a starting point and as documentation. Hardware files use Python class inheritance. +That is, all hardware files inherit methods from a parent file and then add their custom methods. +Hardware files can control other hardware too. +At my shack, I control an AT-200PC antenna tuner, my SDR-IQ, my filter +boxes and my SSB transceiver (using Ethernet) all with Quisk. +Take a look at my n2adr subdirectory. +

+
+

+The quisk_hardware_model.py file shows the basics of hardware control. +There is an open() and close() function called once on startup and +shutdown. The ChangeMode() and ChangeBand() functions are called when +the user changes the mode or band with the corresponding buttons. +The HeartBeat() function is called at about 10 Hz by Quisk. You +can put code there to poll a serial port or to perform other +housekeeping functions. +

+
+

+Here is the start of the SDR-IQ hardware file: +

+
+
+from quisk_hardware_model import Hardware as BaseHardware
+
+class Hardware(BaseHardware):
+  def __init__(self, app, conf):
+    BaseHardware.__init__(self, app, conf)
+    # etc.
+  def ChangeBand(self, band):
+    # etc.
+
+
+

+The file imports the Hardware from quisk_hardware_model and uses it as the basis of the SDR-IQ Hardware class. +It calls the base init function and then adds its own methods for ChangeBand() and other methods. If +it does not define a method, the method from quisk_hardware_model is used. Please refer to Python documentation if +you are not familiar with inheritance. +

+
+

+If you want to write a hardware file from scratch, your file would start the same way. But if you have a radio like +the SDR-IQ but want to customize it, your hardware file would look like this: +

+
+
+from quisk_hardware_sdriq import Hardware as BaseHardware
+
+class Hardware(BaseHardware):
+  def __init__(self, app, conf):
+    BaseHardware.__init__(self, app, conf)
+    # etc.
+  def ChangeBand(self, band):
+    # etc.
+
+
+

+This file uses all the methods from the SDR-IQ file except ones that are defined here. +The HL2 hardware file is in the hermes subdirectory, so to create a custom file you would use: +

+
+
+from hermes.quisk_hardware import Hardware as BaseHardware
+
+
+
+

+Alternatively, you can define a class named "Hardware" in your config +file, +and that class will be used instead of a hardware file. This is +recommended +only for simple hardware needs. The class should start like this: +
+
+

+ +
from quisk_hardware_model import Hardware as BaseHardware
+class Hardware(BaseHardware):
+    def __init__(self, app, conf):
+        BaseHardware.__init__(self, app, conf)
+        # Start your hardware control here.
+        # For ideas, see one of the other hardware modules.
+
+ +
+ +
+

+Both the config file and your hardware file are written in the Python +language. Python is an easy to learn but powerful computer +language. Quisk can be adapted to different hardware because of +the power of Python. +
+
+

+

ChangeFrequency(self, tune, vfo, source='', band='', event=None)

+

+Quisk calls the ChangeFrequency() function when the user changes the Tx +frequency with a mouse click on the graph or waterfall, with the entry +box, with the band Up/Down buttons, etc. The "source" is a string +giving the reason for the change: +
+ +
+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BtnBand A band button was pressed (the +string band is in the band argument)
BtnUpDownThe band Up/Down buttons were +pressed
FreqEntryThe user entered a frequency in +the +box
MouseBtn1Left mouse button was pressed +(for the mouse, "event" is the handler event) +
+
MouseBtn3Right mouse button was pressed
MouseMotionThe user is dragging with the +left button
MouseWheelThe mouse wheel up/down was used
+
+

+Most of the time you will not care about the "source". You just +need to react to the user's action, perhaps by changing the hardware +VFO frequency. It is not necessary to actually make the change +requested. Just +adjust your hardware as required, and return the actual (tune, vfo) +that you want. Quisk will ignore its requested values and use +your actual values instead. +
+ +
+For example, suppose you have a crystal controlled SoftRock. The +VFO frequency is fixed at (say) 7.025 MHz. Then when +ChangeFrequency() is called, return (tune, 7025000). This will +fix your VFO frequency to the only one available. +
+ +
+Suppose Quisk calls ChangeFrequency() with vfo=7050000 and +tune=7100000, so the tune is 50 kHz above the VFO. Suppose that +is unacceptable because of (say) bandwidth limitations, so you want the +VFO closer to the tune. Set your hardware VFO to 7090000 instead, +and return (tune, 7090000). +
+ +
+Suppose Quisk is just controlling a receiver and the audio is +demodulated by the receiver and not by Quisk. Then the center +frequency is always the tuning frequency, and you would set the +receiver frequency to tune, and return (tune, +tune). +
+ +
+

+

ReturnFrequency(self)

+

+When Quisk starts, it calls ReturnFrequency() to get the initial tune +and VFO. To display an initial frequency, return (tune, vfo) on +the first call. +
+ +
+Thereafter, Quisk calls ReturnFrequency() at a 10 Hz rate to poll for +frequency changes. You should almost always return (None, None) +to indicate that the frequencies have not changed since the last time +ReturnFrequency() or ChangeFrequency() was called. Returning +(None, None) is slightly more efficient than returning the actual +frequencies, and thus forcing Quisk to see if its frequencies are out +of date. +
+ +
+The only reason to return something other than (None, None) is if your +hardware can change frequency by itself; that is, other than in +response to ChangeFrequency(). For example, if your hardware is a +receiver with a tuning knob, and the user turns the knob, you must +return the new frequencies from ReturnFrequency() or else Quisk will be +unaware of the change. +
+ +
+

+

Adding Custom Hardware to the Config/Radios Screen

+

+If you have unique hardware and want to add it to the +list of available radio types and add its configuration options to its Config/radios/Hardware screen, +create a subdirectory of the Quisk directory with a name ending in "pkg"; for example, +"myradiopkg". Then put your hardware file in this subdirectory with the name "quisk_hardware.py". +You will need at least one configuration option to specify the hardware file name. +Add this code (all comments) near the top of quisk_hardware.py: +

+
+
+# Define the name of the hardware and the items on the hardware screen (see quisk_conf_defaults.py):
+################ Receivers MyRadio, The special radio that I own
+## hardware_file_name		Hardware file path, rfile
+# This is the file that contains the control logic for each radio.
+#hardware_file_name = 'myradiopkg/quisk_hardware.py'
+
+
+

+Of course, change the names of the radio and subdirectory as appropriate. Your radio of general type "MyRadio" +will now appear in the list of radios on the Config/Radios screen, and its configuration items will appear +on the Config/radio/Hardware screen. You can add additional items. See quisk_conf_defaults.py and the *pkg +subdirectories for examples. + +
+
+

+

Extension Packages

+

+Quisk comes with two extension packages. The freedvpkg package +supports FreeDV digital voice. The n2adr package +supports the hardware in my shack. There are other extension +packages available from third parties. +
+ +
+All extension packages are directories (folders) in the Quisk root; +that is, in the directory where quisk.py is located. This enables +Quisk to find extension modules, and extension modules to find each +other. You can install in a different place, but you will need to +know what you are doing. +
+ +
+Starting with Quisk 3.6 C-language extension modules are not linked +with _quisk.so. Certain symbols from _quisk.so are exported using +the Python CObject or Capsule interface. That simplifies linkage +and eliminates problems with module search paths. See the +documentation in import_quisk_api.c. This change was suggested by +Maitland Bottoms, AA4HS, and he also provided patches. +
+ +
+

+

Shared Libraries

+ +

+The main Python extension module for Quisk is _quisk.so or _quisk.pyd. +It is a shared +library. To import it, it must be on the Python path. There are other +Python extension modules (shared libraries) for other hardware, for +example, +sdriq.so. Quisk works fine when all these modules are in package +subdirectories. If you want to put them somewhere else, be sure that the Python import mechanism can find them. +
+ +
+If you link your sub-packages against _quisk so you can use _quisk functions, be aware that your sub-package must be able +to find +_quisk.so at both compile and run time. You need to follow the Linux rules for searching +for +shared libraries. Try using the "ldd sdriq.so" command to see your +library +dependencies. Also try readelf -d sdriq.so. +
+ +
+For Quisk version 3.6 and newer, you should use the Python CObject or +Capsule mechanism instead of using the C linker to access _quisk +functions and data. Quisk will prepare an array of function and +data pointers and transfer them to your sub-module without using the C +linker. Only minimal changes to your sub-module are +required. The SDR-IQ module sdriq.so uses this method, and you +can use it as a model. See the file import_quisk_api.c for +documentation. +

+
+

New Packages

+

+If you have more complex needs or want to distribute your code more +widely, you need to create a new Quisk package. That is easily +done by modeling your code after the existing packages. To create +a new package you need a subdirectory of the Quisk root to hold it, +perhaps "mypak". Then create these files in mypak: +
+ +
+

+
    + +
  • __init__.py This file just consists of +the +character "#". Its existence identifies mypak as a Python package. +
    +
  • +
+
    + +
  • makefile Only needed if you have C-language extensions. +
    +
  • +
+
    + +
  • MANIFEST.in A list of files you want to +distribute in your package. This file often consists of just one +line: include *.c *.h *.py *.txt +*.html +*.so makefile +
    +
  • +
+
    + +
  • README.txt This file is expected to +be +present.
  • +
+ +
+

+To these files, you add all your Python files, C-language files and any +other files you need. If you have a hardware or widget file, they +should be named quisk_hardware.py and quisk_widgets.py. Longer +names are not needed because you are within a package. You should +include a sample quisk_conf.py too. +
+ +
+To compile C-language extensions (if you have any) enter "make". +To import your hardware and widgets files from other modules, use: +
+ +
+ from mypak import quisk_hardware +
+ from mypak import quisk_widgets +
+ from mypak import myext as EXT +
+ +
+The setup.py file describes how to build your package. But it is +also used to distribute it. To create a mypak-1.0.tar.gz file in +the "dist" subdirectory, use: +
+ +
+ python setup.py sdist +
+ +
+You can then put the file on your web page (for example). To make +your package available on PyPi.Python.org, first register with PyPi and +then use: +
+ +
+ python setup.py register sdist upload +
+ +
+Python supports quite complicated packages; see the distutils +documentation. +
+ +
+

+

Installing Packages

+

+Your package mypak will run on your machine as is. But when +another user gets mypak-1.0.tar.gz they need to install it. +Basically, they just put it in the Quisk root with the same name as on +your machine. Here is an INSTALL.txt: +
+ +
+Unzip and untar this archive at the root of the Quisk directory; that +is, where the file quisk.py is located. In this example, the +archive is named "mypak" and the path to quisk.py is +/home/jim/quisk/quisk.py. +
+ +
+ mv mypak-1.0.tar.gz /home/jim/quisk +
+ cd /home/jim/quisk +
+ gunzip mypak-1.0.tar.gz +
+ tar xf mypak-1.0.tar +
+ # Make sure that directory mypak-1.0 exists before removing the +archive. +
+ rm mypak-1.0.tar # tar file +is no longer needed +
+ mv mypak-1.0 mypak # change to the correct name +
+ +
+

+

Digital Modes

+

+Quisk has a number of modes "DGT-" to receive and transmit digital +signals. The modes "DGT-U" and "DGT-L" decode the signal as upper +or lower sideband, and send the stereo audio to the digital sound +device. The left and right channel are the same. +The bandwidth is set with the filter buttons as +usual, and the filter center is 1500 Hertz. +The mode "DGT-IQ" does not decode the audio; the I/Q +samples are sent directly to the stereo digital sound device. +
+ +
+Digital modes require an external digital program such as Fldigi or WSJT-X to decode the received +audio and to generate transmit audio. There are two aspects, rig +control and audio transfer. Rig control is needed to synchronize +the transmit frequency between Quisk and WSJT-X and +to operate the PTT (push to talk). You can control Quisk using +XML-RPC, Hamlib or a serial port. +See Rig Control and Logging below. +
+
+Quisk has additional audio inputs and outputs for digital programs. +On the radio Sound screen "Digital Tx0 Input" is the Quisk input for transmitted audio. +The "Digital Rx0 Output" is the Quisk output of the received digital signals. +You need to set these names to a sound device. + +The sound device is not a real sound card; it is +some sort of loopback device, and is only needed because there is no +standard way of sending digital samples between two programs (yet). +The method to use for Quisk is the same as for other programs, and is +on the web. It works for any digital program. +
+
+If you use Windows, you need to purchase a Virtual Audio Cable +(VAC). Connect Quisk to one side, and your digital program to the other. +The name of the sound device depends on which VAC you use. +
+
+If you use Linux, you can use the ALSA loopback device, or use +PortAudio, PulseAudio or Jack to route your audio. Using PulseAudio is the +easiest method because Quisk can set up the loopback devices when it starts. +Set "Digital Tx0 Input" to "pulse: Use name QuiskDigitalInput". +Set "Digital Rx0 Output" to "pulse: Use name QuiskDigitalOutput.monitor". +These names are on the drop down list for the sound device. +In your digital program, connect the digital +input to QuiskDigitalOutput.monitor and the digital output to QuiskDigitalInput. +These names will be on the sound menu of the digital program, and you should be receiving and transmitting digital data. +Remember to select one of the DGT-* modes. +
+
+Fldigi only has a PulseAudio check box, and there is no way to set the proper device. +In this case, first install the program pavucontrol to control PulseAudio. This is a useful program to +control and understand PulseAudio even if you are not using digital. Set the Quisk devices as above. +Now start both Quisk and Fldigi, and then pavucontrol. The Playback and Recording screens in pavucontrol will +show the devices being used. Change the Fldigi playback to QuiskDigitalInput, and the Fldigi recording to +Monitor of QuiskDigitalOutput. Then everything should work. You do not need to use pavucontrol again because PulseAudio will +remember the settings. +
+ +
+If you don't have PulseAudio or don't want to use it, you can use the ALSA loopback device. +The ALSA loopback device works the same way as the Windows VAC. First +create the loopback device with the command "modprobe snd-aloop" (you +will need to be root). You can create the loopback device +when the system starts, but the way to do that depends on your version of +Linux. I added snd-aloop to /etc/modules. You could put the modprobe command in /etc/rc.local instead. +Restart Linux. Now you can enter "cat /proc/asound/cards" to +print out your sound cards, and you should see a "Loopback" card +listed. The cards are also shown on the Quisk Sound +screen. The Loopback card has one side that connects to Quisk and +another side that connects to your digital program. For the Quisk +side connect both Digital Input and Digital Output to Loopback,0. +Note that the Loopback card is full duplex, and handles both +input and output. There are actually eight loopbacks created at once, +but we are only using subdevice 0. For the digital program side, set the input and +output to "Loopback,1". +Your audio is now connected and you +should be able to receive digital signals. Be sure to test your +transmit signal off the air. You may need to reduce power to +improve linearity. +
+ +
+

+

Rig Control and Logging

+

+Digital mode and logging programs need to control Quisk to read and set the frequency and mode and to operate PTT. +Quisk has three options for external control and they can all be used together to connect to multiple programs. +See the Config/radio/Remote screen. +See http://james.ahlstrom.name/hamlib.html for more information. +

+To connect an external program to Quisk using Hamlib, configure your program to use "Hamlib NET rigctl" (rig 2). +Then go to the Quisk "Remote" config screen for your radio and set +"IP address for Hamlib Rig 2" to "localhost", and set +"IP port for Hamlib" to 4532. This assumes you are not using the rigctld daemon program. If you are, +set the Quisk port to 4575 and tell rigctld to control quisk on port 4575. +Now changing the frequency on one program will change the other. +Keying Quisk to key down (however you do that with your hardware) will +set the external program to Transmit. Pressing the PTT control in the external program will +also press the PTT button in Quisk. +

+You can also control Quisk from another program by using the XML-RPC method if this is available +in your program. Fldigi can use this method. +

+If your program only uses a serial port (N1MM+) then +use Hamlib with the rig set to "Flex" and connect to the Quisk serial port set on the Remote screen. +For Linux, Quisk can set up these ports itself, and they have names like "/tmp/QuiskTTY0". On Windows +you need a "Virtual Serial Port" that is set up by an external program. This is like the "Virtual Audio Cable" +needed for samples. An Internet search will turn up HDD Software, Eltima Software and many others. Set up a port pair, +and enter one name on the Quisk Remote screen and the other name in the external program. +
+
+

+

WSJT-X

+

+Quisk has special support for the digital program WSJT-X. +There is a button to start WSJT-X on the Config/Config screen. Look for "Start WSJT-X" and select "Main Rx0 now". +This starts WSJT-X with radio "quisk" and you will see "WSJT-X - quisk" in the title bar. +Then under WSJT-X File/Settings set up the Radio and Audio as described above. +For Linux, use the pulseaudio devices: +

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Quisk NameQuisk ValueWSJT-X NameWSJT-X Value
IP address for Hamlib Rig 2localhostRadio RigHamlib NET rigctl
IP port for Hamlib4532Network Serverlocalhost:4532
Digital Tx0 InputUse name QuiskDigitalInputAudio Soundcard OutputQuiskDigitalInput
Digital Rx0 OutputUse name QuiskDigitalOutput.monitorAudio Soundcard InputQuiskDigitalOutput.monitor
+

+

Vector Network Analyzer

+

+If you have my transceiver hardware from 2010 QEX, or the newer HiQSDR +hardware you can use it as a vector network analyzer by using a +special program. As of 2021 the HL2 no longer works as a VNA. This may change in the future. +Run the VNA program with "python quisk_vna.py" or use a +shortcut. The VNA program will not work with SoftRock or other hardware. +This VNA program enables you to analyze your antennas without additional expense. +
+ +
+A calibration run must be taken before any data can be obtained. +The calibrations request a scan of data points every 15 killohertz from +zero to 60 megahertz, or a little over 4000 points. These data +are saved so that the scan frequencies can be changed without a new +calibration. For any start and end scan frequency the user +chooses, these saved calibrations are used with linear interpolation. +
+ +
+

+

HiQSDR Internals

+

+When running in VNA mode the two control bytes [18:20] are the 16-bit +non-zero VNA count "vna_count", the number of data points to +send. This locks the transmit and receive frequency to the +same value. The phases are also equal except for a fixed time +delay, which causes a linear change of phase with frequency. The +starting frequency is the receive frequency (actually phase) +rx_phase. Subsequent points have the transmit phase tx_phase +added to create a frequency scan. Specifically, after each data +point, the tx_phase is added to create the RF output at the next +frequency; then there is a pause of 65 microseconds to allow the +external device under test to stabilize; then 4096 data points are +added together to create the sample; then the sample is added to the +block of data to send by UDP. A sample of zero is sent after the +last data point, and the process repeats. The receiving software +must look for the zero sample that marks the start of a new scan. +The total number of points in the scan is vna_count, and blocks +received with a different length should be rejected. +Since the transmit and receive frequency are the same, the data points +are I/Q values at DC; that is, a complex number representing a voltage +and phase. +If vna_count is zero, the firmware is operating normally, and not in VNA mode. +
+
+ +

+

Remote Operation

+

+Ben, AC2YD, contributed a feature that enables you to run a radio at a remote location from a control +head. The remote radio could be on your local area network or somewhere on the Internet. The remote radio +is connected to a computer running Quisk, the computer could run Linux or Windows, and it could be +a single-board computer such as Raspberry Pi. The two computers communicate using the TCP and UDP protocols. +You can use either a wired connection or WiFi. +The feature uses TCP port 4585 and two UDP ports 4586 and 4587. These must be open at the remote radio. +It should not be necessary to open ports at the control head as it initiates all communication. +A paper describing the original design is here. +
+
+Nigel, G4ZAL wrote a quick guide here. +

+
+

+The version of Quisk running on the control head must exactly match the version at the remote radio. +Also, each type of radio has its own remote hardware file and control head hardware file. The supported +radios are softrock, hermes and hiqsdr. +

+
+

+If you have dropouts in the sound, try increasing the "Play latency msec" on the Config/Timing screen. Quisk +buffers sound, and the buffer size can be adjusted. +

+
+

+Create a radio of type "Control Head", and on the Radio/Remote screen enter the IP address or host name +of the remote radio and a password. Change the hardware file to ac2yd/control_softrock.py or +ac2yd/control_hermes.py or ac2yd/control_hiqsdr.py. Enter the usual widget file for that radio if any. +Create sound devices for radio sound and microphone sound. Do not create devices for samples or digital modes. +

+
+

+Create a radio of the correct type at the remote site. This must be softrock, hermes or hiqsdr. +Configure the radio to correctly control the hardware. You should be able to operate successfully at the remote. +Then change the hardware file to ac2yd/remote_softrock.py or ac2yd/remote_hermes.py or ac2yd/remote_hiqsdr.py. Enter the usual +widget file if any. The radio will still operate normally unless the control head is connected. Enter +the same password on the Radio/Remote screen. The password only needs to be entered once at each end. +It is a "shared secret". Use a somewhat lengthy pass phrase of random words, at least 20 characters long. +

+
+

+You can use Windows, Mac or Linux at either end. But for Windows, the Windows Firewall will interfere. +Do not turn the firewall off. Change the network profile type from the default Public to Private if +you are on a private home network. The setting is under Settings, Network, WiFi or Ethernet, Network Profile. +If you are on a public network, you should use a VPN for security. +

+
+

+For security, Quisk will not accept remote operation unless the special hardware files are used. And the +password or pass phrase must match. But you must (as usual) make sure both computers are secure and can not +be broken into. You must (as usual) have a good router password and secure settings to protect your home network. +

+
+

+The AC2YD remote feature was designed to send back demodulated audio, not samples, in order to reduce the +bandwidth to a minimum. Running digital modes on the control head would require more data streams for +digital in/out. Also, note that recording samples to a file fails for the same reason. And most hardware +config settings are inoperative on the control head. I can add some more interaction like a clip indicator, +but complete control, such as adding recording samples, is a never-ending list. +

+
+

+Because of the low bandwidth and the time stamps on the CW keying, the AC2YD feature achieves fast classic ham +radio over a network. For digital modes, it seems workable to run everything on the remote and control it with +remote terminal software because fast response is not required. +

+
+
+ + + + + diff --git a/dxcluster.py b/dxcluster.py new file mode 100644 index 0000000..eff52d7 --- /dev/null +++ b/dxcluster.py @@ -0,0 +1,141 @@ +from __future__ import absolute_import +# This code was contributed by Christof, DJ4CM. Many Thanks!! + +import threading +import time +import telnetlib +import quisk_conf_defaults as conf + +class DxEntry: + def __init__(self): + self.info = [] + + def getFreq(self): + return self.freq + + def getDX(self): + return self.dx + + def getSpotter(self, index): + return self.info[index][0] + + def getTime(self, index): + return self.info[index][1] + + def getLocation(self, index): + return self.info[index][2] + + def getComment(self, index): + return self.info[index][3] + + def getLen(self): + return len(self.info) + + def equal(self, element): + if element.getDX() == self.dx: + return True + else: + return False + + def join (self, element): + for i in range (0, len(element.info)): + self.info.insert(0, element.info[i]) + length = len(self.info) + # limit to max history + if length > 3: + del (self.info[length-1]) + self.timestamp = max (self.timestamp, element.timestamp) + + def isExpired(self): + return time.time()-self.timestamp > conf.dxClExpireTime * 60 + + def parseMessage(self, message): + words = message.split() + sTime = '' + locator = '' + comment = '' + if len(words) > 3 and words[0].lower() == 'dx' and words[1].lower() == 'de': + spotter = words[2].strip(':') + self.freq = int(float(words[3])*1000) + self.dx = words[4] + for index in range (5, len(words)): + word = words[index] + try: + if sTime != '': + locator = word.strip('\07') + #search time + if word[0:3].isdigit() and word[4].isalpha(): + sTime = word.strip('\07') + sTime = sTime[0:2]+':'+sTime[2:4]+ ' UTC' + if sTime == '': + if comment != '': + comment += ' ' + comment += word + except: + pass + self.info.insert(0, (spotter, sTime, locator, comment)) + self.timestamp = time.time() + #print(self.dx, self.freq, spotter, sTime, locator, comment) + return True + return False + +class DxCluster(threading.Thread): + def __init__(self): + self.do_init = 1 + threading.Thread.__init__(self) + self.doQuit = threading.Event() + self.dxSpots = [] + self.doQuit.clear() + + def run(self): + self.telnetInit() + self.telnetConnect() + while not self.doQuit.isSet(): + try: + self.telnetRead() + except: + self.tn.close() + time.sleep(20) + if not self.doQuit.isSet(): + self.telnetConnect() + self.tn.close() + + def setListener (self, listener): + self.listener = listener + + def telnetInit(self): + self.tn = telnetlib.Telnet() + + def telnetConnect(self): + for i in range(10): + try: + self.tn.open(conf.dxClHost, conf.dxClPort, 10) + self.tn.read_until(b"login:", 10) + self.tn.write(conf.user_call_sign.encode('utf-8', errors='ignore') + b"\n") # user_call_sign may be Unicode + break + except: + time.sleep(0.5) + if conf.dxClPassword: + self.tn.read_until(b"Password: ") + self.tn.write(conf.dxClPassword.encode('utf-8', errors='ignore') + b"\n") + + def telnetRead(self): + message = self.tn.read_until(b'\n', 60).decode(encoding='utf-8', errors='replace') + if self.doQuit.isSet() == False: + dxEntry = DxEntry(); + if dxEntry.parseMessage(message): + for i, listElement in enumerate(self.dxSpots): + if (listElement.equal(dxEntry)): + listElement.join (dxEntry) + return + if listElement.isExpired(): + del (self.dxSpots[i]) + self.dxSpots.append(dxEntry) + if self.listener: + self.listener() + + def getHost(self): + return self.tn.host + ':' + str(self.tn.port) + + def stop(self): + self.doQuit.set() diff --git a/extdemod.c b/extdemod.c new file mode 100644 index 0000000..26636d4 --- /dev/null +++ b/extdemod.c @@ -0,0 +1,47 @@ +#include +#include +#include +#include +#include "quisk.h" + +// If you set add_extern_demod in your config file, you will get another +// button that will call this module. Change it to what you want. Save +// a copy because new releases of Quisk will overwrite this file. +// +// NOTE: NEW RELEASES OF QUISK WILL OVERWRITE THIS FILE! + +int quisk_extern_demod(complex double * cSamples, int nSamples, double decim) +{ // Filter and demodulate the I/Q samples into audio play samples. +// cSamples: The input I/Q samples, and the output stereo play samples. +// nSamples: The number of input samples; maximum is SAMP_BUFFER_SIZE. +// decim: The decimation needed (1.0 for no decimation). +// The output play samples are stereo, and are placed into cSamples. +// The return value is the number of output samples = nSamples / decim. +// See quisk.h for useful data in quisk_sound_state. For example, the +// sample rate is quisk_sound_state.sample_rate. If you need decimation, +// look at iDecimate() and fDecimate() in quisk.c. + + int i; + double d, di; + complex double cx; + static complex double fm_1 = 10; // Sample delayed by one + static complex double fm_2 = 10; // Sample delayed by two + + if (fabs (decim - 1.0) > 0.001) // no provision for decimation + return 0; + + for (i = 0; i < nSamples; i++) { // narrow FM + cx = cSamples[i]; + di = creal(fm_1) * (cimag(cx) - cimag(fm_2)) - + cimag(fm_1) * (creal(cx) - creal(fm_2)); + d = creal(fm_1) * creal(fm_1) + cimag(fm_1) * cimag(fm_1); + if (d == 0) // I don't think this can happen + di = 0; + else + di = di / d * quisk_sound_state.sample_rate; + fm_2 = fm_1; // fm_2 is sample cSamples[i - 2] + fm_1 = cx; // fm_1 is sample cSamples[i - 1] + cSamples[i] = di + I * di; // monophonic sound, two channels + } + return nSamples; // Number of play samples +} diff --git a/filter.c b/filter.c new file mode 100644 index 0000000..670d2c9 --- /dev/null +++ b/filter.c @@ -0,0 +1,489 @@ +#include +#include +#include +#include // Use native C99 complex type for fftw3 +#include "quisk.h" +#include "filter.h" +#include "filters.h" + +void quisk_filt_cInit(struct quisk_cFilter * filter, double * coefs, int taps) +{ // Prepare a new filter using coefs and taps. Samples are complex. + filter->dCoefs = coefs; + filter->cpxCoefs = NULL; + filter->cSamples = (complex double *)malloc(taps * sizeof(complex double)); + memset(filter->cSamples, 0, taps * sizeof(complex double)); + filter->ptcSamp = filter->cSamples; + filter->nTaps = taps; + filter->decim_index = 0; + filter->cBuf = NULL; + filter->nBuf = 0; +} + +void quisk_filt_dInit(struct quisk_dFilter * filter, double * coefs, int taps) +{ // Prepare a new filter using coefs and taps. Samples are double. + filter->dCoefs = coefs; + filter->cpxCoefs = NULL; + filter->dSamples = (double *)malloc(taps * sizeof(double)); + memset(filter->dSamples, 0, taps * sizeof(double)); + filter->ptdSamp = filter->dSamples; + filter->nTaps = taps; + filter->decim_index = 0; + filter->dBuf = NULL; + filter->nBuf = 0; +} + +void quisk_filt_differInit(struct quisk_dFilter * filter, int taps) +{ // Prepare a new classic differentiating filter. taps must be odd. + int j, k; + + filter->dCoefs = (double *)malloc(taps * sizeof(double)); + for (k = - (taps - 1) / 2; k <= (taps - 1) / 2; k++) { + j = (taps - 1) / 2 + k; + if (k == 0) + filter->dCoefs[j] = 0; + else + filter->dCoefs[j] = pow(-1, k) / k; + printf("%4d taps %8.4lf\n", j, filter->dCoefs[j]); + } + filter->cpxCoefs = NULL; + filter->dSamples = (double *)malloc(taps * sizeof(double)); + memset(filter->dSamples, 0, taps * sizeof(double)); + filter->ptdSamp = filter->dSamples; + filter->nTaps = taps; + filter->decim_index = 0; + filter->dBuf = NULL; + filter->nBuf = 0; +} + +void quisk_filt_tune(struct quisk_dFilter * filter, double freq, int ssb_upper) +{ // Tune a filter into an analytic I/Q filter with complex coefficients. + // freq is the center frequency / sample rate. Reverse coef if ssb_upper == 0. + // This is used for both quisk_dFilter and quisk_cFilter with a cast. + // Filter can be re-tuned repeatedly. + // + // The tuned low pass filter has a loss of 0.5 when applied to real signals. + // There is no loss applied to complex signals. Coeffs of the tuned filter are not symetric(??). + int i; + complex double coef, tune; + double D; + + if ( ! filter->cpxCoefs) + filter->cpxCoefs = (complex double *)malloc(filter->nTaps * sizeof(complex double)); + tune = I * 2.0 * M_PI * freq; + D = (filter->nTaps - 1.0) / 2.0; + for (i = 0; i < filter->nTaps; i++) { + coef = cexp(tune * (i - D)) * filter->dCoefs[i]; + if (ssb_upper) + filter->cpxCoefs[i] = coef; + else + filter->cpxCoefs[i] = cimag(coef) + I * creal(coef); + } +} + +complex double quisk_dC_out(double sample, struct quisk_dFilter * filter) +{ + complex double csample; + complex double * ptCoef; + double * ptSample; + int k; + + // FIR bandpass filter; separate double sample into I and Q. + // Put samples into buffer left to right. Use samples right to left. + ptSample = filter->ptdSamp; + *ptSample = sample; + ptCoef = filter->cpxCoefs; + csample = 0; + for (k = 0; k < filter->nTaps; k++, ptCoef++) { + csample += *ptSample * *ptCoef; + if (--ptSample < filter->dSamples) + ptSample = filter->dSamples + filter->nTaps - 1; + } + if (++filter->ptdSamp >= filter->dSamples + filter->nTaps) + filter->ptdSamp = filter->dSamples; + return csample; +} + +#if 0 +complex double quisk_cC_out(complex double sample, struct quisk_cFilter * filter) +{ + complex double csample; + complex double * ptCoef; + complex double * ptSample; + int k; + + // FIR bandpass filter; filter complex samples by complex coeffs. + // Put samples into buffer left to right. Use samples right to left. + ptSample = filter->ptcSamp; + *ptSample = sample; + ptCoef = filter->cpxCoefs; + csample = 0; + for (k = 0; k < filter->nTaps; k++, ptCoef++) { + csample += *ptSample * *ptCoef; + if (--ptSample < filter->cSamples) + ptSample = filter->cSamples + filter->nTaps - 1; + } + if (++filter->ptcSamp >= filter->cSamples + filter->nTaps) + filter->ptcSamp = filter->cSamples; + return csample; +} +#endif + +int quisk_cInterpolate(complex double * cSamples, int count, struct quisk_cFilter * filter, int interp) +{ // This uses the double coefficients of filter (not the complex). Samples are complex. + int i, j, k, nOut; + double * ptCoef; + complex double * ptSample; + complex double csample; + + if (count > filter->nBuf) { // increase size of sample buffer + filter->nBuf = count * 2; + if (filter->cBuf) + free(filter->cBuf); + filter->cBuf = (complex double *)malloc(filter->nBuf * sizeof(complex double)); + } + memcpy(filter->cBuf, cSamples, count * sizeof(complex double)); + nOut = 0; + for (i = 0; i < count; i++) { + // Put samples into buffer left to right. Use samples right to left. + *filter->ptcSamp = filter->cBuf[i]; + for (j = 0; j < interp; j++) { + ptSample = filter->ptcSamp; + ptCoef = filter->dCoefs + j; + csample = 0; + for (k = 0; k < filter->nTaps / interp; k++, ptCoef += interp) { + csample += *ptSample * *ptCoef; + if (--ptSample < filter->cSamples) + ptSample = filter->cSamples + filter->nTaps - 1; + } + if (nOut < SAMP_BUFFER_SIZE * 8 / 10) + cSamples[nOut++] = csample * interp; + } + if (++filter->ptcSamp >= filter->cSamples + filter->nTaps) + filter->ptcSamp = filter->cSamples; + } + return nOut; +} + +int quisk_dInterpolate(double * dSamples, int count, struct quisk_dFilter * filter, int interp) +{ // This uses the double coefficients of filter (not the complex). Samples are double. + int i, j, k, nOut; + double * ptCoef; + double * ptSample; + double dsample; + + if (count > filter->nBuf) { // increase size of sample buffer + filter->nBuf = count * 2; + if (filter->dBuf) + free(filter->dBuf); + filter->dBuf = (double *)malloc(filter->nBuf * sizeof(double)); + } + memcpy(filter->dBuf, dSamples, count * sizeof(double)); + nOut = 0; + for (i = 0; i < count; i++) { + // Put samples into buffer left to right. Use samples right to left. + *filter->ptdSamp = filter->dBuf[i]; + for (j = 0; j < interp; j++) { + ptSample = filter->ptdSamp; + ptCoef = filter->dCoefs + j; + dsample = 0; + for (k = 0; k < filter->nTaps / interp; k++, ptCoef += interp) { + dsample += *ptSample * *ptCoef; + if (--ptSample < filter->dSamples) + ptSample = filter->dSamples + filter->nTaps - 1; + } + if (nOut < SAMP_BUFFER_SIZE * 8 / 10) + dSamples[nOut++] = dsample * interp; + } + if (++filter->ptdSamp >= filter->dSamples + filter->nTaps) + filter->ptdSamp = filter->dSamples; + } + return nOut; +} + +int quisk_cDecimate(complex double * cSamples, int count, struct quisk_cFilter * filter, int decim) +{ // This uses the double coefficients of filter (not the complex). + int i, k, nOut; + complex double * ptSample; + double * ptCoef; + complex double csample; + + nOut = 0; + for (i = 0; i < count; i++) { + *filter->ptcSamp = cSamples[i]; + if (++filter->decim_index >= decim) { + filter->decim_index = 0; // output a sample + csample = 0; + ptSample = filter->ptcSamp; + ptCoef = filter->dCoefs; + for (k = 0; k < filter->nTaps; k++, ptCoef++) { + csample += *ptSample * *ptCoef; + if (--ptSample < filter->cSamples) + ptSample = filter->cSamples + filter->nTaps - 1; + } + cSamples[nOut++] = csample; + } + if (++filter->ptcSamp >= filter->cSamples + filter->nTaps) + filter->ptcSamp = filter->cSamples; + } + return nOut; +} + +int quisk_cCDecimate(complex double * cSamples, int count, struct quisk_cFilter * filter, int decim) +{ // This uses the complex coefficients of filter (not the double). Call quisk_filt_tune() first. + int i, k, nOut; + complex double * ptSample; + complex double * ptCoef; + complex double csample; + + nOut = 0; + for (i = 0; i < count; i++) { + *filter->ptcSamp = cSamples[i]; + if (++filter->decim_index >= decim) { + filter->decim_index = 0; // output a sample + csample = 0; + ptSample = filter->ptcSamp; + ptCoef = filter->cpxCoefs; + for (k = 0; k < filter->nTaps; k++, ptCoef++) { + csample += *ptSample * *ptCoef; + if (--ptSample < filter->cSamples) + ptSample = filter->cSamples + filter->nTaps - 1; + } + cSamples[nOut++] = csample; + } + if (++filter->ptcSamp >= filter->cSamples + filter->nTaps) + filter->ptcSamp = filter->cSamples; + } + return nOut; +} + +int quisk_dDecimate(double * dSamples, int count, struct quisk_dFilter * filter, int decim) +{ // This uses the double coefficients of filter (not the complex). + int i, k, nOut; + double * ptSample; + double * ptCoef; + double dsample; + + nOut = 0; + for (i = 0; i < count; i++) { + *filter->ptdSamp = dSamples[i]; + if (++filter->decim_index >= decim) { + filter->decim_index = 0; // output a sample + dsample = 0; + ptSample = filter->ptdSamp; + ptCoef = filter->dCoefs; + for (k = 0; k < filter->nTaps; k++, ptCoef++) { + dsample += *ptSample * *ptCoef; + if (--ptSample < filter->dSamples) + ptSample = filter->dSamples + filter->nTaps - 1; + } + dSamples[nOut++] = dsample; + } + if (++filter->ptdSamp >= filter->dSamples + filter->nTaps) + filter->ptdSamp = filter->dSamples; + } + return nOut; +} + +int quisk_cInterpDecim(complex double * cSamples, int count, struct quisk_cFilter * filter, int interp, int decim) +{ // Interpolate by interp, and then decimate by decim. + // This uses the double coefficients of filter (not the complex). Samples are complex. + int i, k, nOut; + double * ptCoef; + complex double * ptSample; + complex double csample; + + if (count > filter->nBuf) { // increase size of sample buffer + filter->nBuf = count * 2; + if (filter->cBuf) + free(filter->cBuf); + filter->cBuf = (complex double *)malloc(filter->nBuf * sizeof(complex double)); + } + memcpy(filter->cBuf, cSamples, count * sizeof(complex double)); + nOut = 0; + for (i = 0; i < count; i++) { + // Put samples into buffer left to right. Use samples right to left. + *filter->ptcSamp = filter->cBuf[i]; + while (filter->decim_index < interp) { + ptSample = filter->ptcSamp; + ptCoef = filter->dCoefs + filter->decim_index; + csample = 0; + for (k = 0; k < filter->nTaps / interp; k++, ptCoef += interp) { + csample += *ptSample * *ptCoef; + if (--ptSample < filter->cSamples) + ptSample = filter->cSamples + filter->nTaps - 1; + } + if (nOut < SAMP_BUFFER_SIZE * 8 / 10) + cSamples[nOut++] = csample * interp; + filter->decim_index += decim; + } + if (++filter->ptcSamp >= filter->cSamples + filter->nTaps) + filter->ptcSamp = filter->cSamples; + filter->decim_index = filter->decim_index - interp; + } + return nOut; +} + +double quisk_dD_out(double samp, struct quisk_dFilter * filter) +{ // Filter double samples. + int k; + double * ptSample; + double * ptCoef; + double dsample; + + *filter->ptdSamp = samp; + dsample = 0; + ptSample = filter->ptdSamp; + ptCoef = filter->dCoefs; + for (k = 0; k < filter->nTaps; k++, ptCoef++) { + dsample += *ptSample * *ptCoef; + if (--ptSample < filter->dSamples) + ptSample = filter->dSamples + filter->nTaps - 1; + } + if (++filter->ptdSamp >= filter->dSamples + filter->nTaps) + filter->ptdSamp = filter->dSamples; + return dsample; +} + +int quisk_dFilter(double * dSamples, int count, struct quisk_dFilter * filter) +{ // Filter double samples. + int i, k, nOut; + double * ptSample; + double * ptCoef; + double dsample; + + nOut = 0; + for (i = 0; i < count; i++) { + *filter->ptdSamp = dSamples[i]; + dsample = 0; + ptSample = filter->ptdSamp; + ptCoef = filter->dCoefs; + for (k = 0; k < filter->nTaps; k++, ptCoef++) { + dsample += *ptSample * *ptCoef; + if (--ptSample < filter->dSamples) + ptSample = filter->dSamples + filter->nTaps - 1; + } + dSamples[nOut++] = dsample; + if (++filter->ptdSamp >= filter->dSamples + filter->nTaps) + filter->ptdSamp = filter->dSamples; + } + return nOut; +} + +int quisk_cFilter(complex double * cSamples, int count, struct quisk_cFilter * filter) +{ // Filter complex samples using the double coefficients of filter (not the complex). + return quisk_cDecimate(cSamples, count, filter, 1); +} + +int quisk_cDecim2HB45(complex double * cSamples, int count, struct quisk_cHB45Filter * filter) +{ // This uses the double coefficients of filter (not the complex). +// Half band filter, sample rate 96 Hz, pass 16, center 24, stop 32, good BW 2/3, 45 taps. + int i, nOut; + complex double * samples, * center; + static double coef[12] = { 0.000018566625444266, -0.000118469698701817, 0.000457318798253456, + -0.001347840471412094, 0.003321838571445455, -0.007198422696929033, 0.014211106939802483, + -0.026424776824073383, 0.048414810444971007, -0.096214669073304823, 0.314881034738348550, + 0.500000000000000000 }; // Rate 96, cutoff 16-24-32, atten 120 dB. Coef[0] and [44] are zero. + + nOut = 0; + samples = filter->samples; + center = filter->center; + for (i = 0; i < count; i++) { + if (filter->toggle == 0){ + filter->toggle = 1; + memmove(center + 1, center, sizeof(complex double) * 10); + center[0] = cSamples[i]; + } + else { + filter->toggle = 0; + memmove(samples + 1, samples, sizeof(complex double) * 21); + samples[0] = cSamples[i]; + // output a sample + cSamples[nOut++] = + (samples[ 0] + samples[21]) * coef[0] + + (samples[ 1] + samples[20]) * coef[1] + + (samples[ 2] + samples[19]) * coef[2] + + (samples[ 3] + samples[18]) * coef[3] + + (samples[ 4] + samples[17]) * coef[4] + + (samples[ 5] + samples[16]) * coef[5] + + (samples[ 6] + samples[15]) * coef[6] + + (samples[ 7] + samples[14]) * coef[7] + + (samples[ 8] + samples[13]) * coef[8] + + (samples[ 9] + samples[12]) * coef[9] + + (samples[10] + samples[11]) * coef[10] + + center[10] * coef[11]; + } + } + return nOut; +} + + +int quisk_dInterp2HB45(double * dsamples, int count, struct quisk_dHB45Filter * filter) +{ // Half-Band interpolation by 2 + int i, k, nOut, nCoef, nSamp; + double out; + double * samples; + static double coef[12] = { 0.000018566625444266, -0.000118469698701817, 0.000457318798253456, + -0.001347840471412094, 0.003321838571445455, -0.007198422696929033, 0.014211106939802483, + -0.026424776824073383, 0.048414810444971007, -0.096214669073304823, 0.314881034738348550, + 0.500000000000000000 }; // Rate 96, cutoff 16-24-32, atten 120 dB. Coef[0] and [44] are zero. + + if (count > filter->nBuf) { // increase size of sample buffer + filter->nBuf = count * 2; + if (filter->dBuf) + free(filter->dBuf); + filter->dBuf = (double *)malloc(filter->nBuf * sizeof(double)); + } + nCoef = 12; + nSamp = (nCoef - 1) * 2; + memcpy(filter->dBuf, dsamples, count * sizeof(double)); + samples = filter->samples; + nOut = 0; + for (i = 0; i < count; i++) { + memmove(samples + 1, samples, (nSamp - 1) * sizeof(double)); + samples[0] = filter->dBuf[i]; + if (nOut > SAMP_BUFFER_SIZE * 8 / 10) + continue; + dsamples[nOut++] = samples[nCoef - 1] * coef[nCoef - 1] * 2; + out = 0; + for (k = 0; k < nSamp / 2; k++) + out += (samples[k] + samples[nSamp - 1 - k]) * coef[k]; + dsamples[nOut++] = out * 2; + } + return nOut; +} + +int quisk_cInterp2HB45(complex double * cSamples, int count, struct quisk_cHB45Filter * filter) +{ // Half-Band interpolation by 2 + int i, k, nOut, nCoef, nSamp; + complex double out; + complex double * samples; + static double coef[12] = { 0.000018566625444266, -0.000118469698701817, 0.000457318798253456, + -0.001347840471412094, 0.003321838571445455, -0.007198422696929033, 0.014211106939802483, + -0.026424776824073383, 0.048414810444971007, -0.096214669073304823, 0.314881034738348550, + 0.500000000000000000 }; // Rate 96, cutoff 16-24-32, atten 120 dB. Coef[0] and [44] are zero. + + if (count > filter->nBuf) { // increase size of sample buffer + filter->nBuf = count * 2; + if (filter->cBuf) + free(filter->cBuf); + filter->cBuf = (complex double *)malloc(filter->nBuf * sizeof(complex double)); + } + nCoef = 12; + nSamp = (nCoef - 1) * 2; + memcpy(filter->cBuf, cSamples, count * sizeof(complex double)); + samples = filter->samples; + nOut = 0; + for (i = 0; i < count; i++) { + memmove(samples + 1, samples, (nSamp - 1) * sizeof(complex double)); + samples[0] = filter->cBuf[i]; + if (nOut > SAMP_BUFFER_SIZE * 8 / 10) + continue; + cSamples[nOut++] = samples[nCoef - 1] * coef[nCoef - 1] * 2; + out = 0; + for (k = 0; k < nSamp / 2; k++) + out += (samples[k] + samples[nSamp - 1 - k]) * coef[k]; + cSamples[nOut++] = out * 2; + } + return nOut; +} + diff --git a/filter.h b/filter.h new file mode 100644 index 0000000..f806f25 --- /dev/null +++ b/filter.h @@ -0,0 +1,86 @@ +struct quisk_cFilter { + double * dCoefs; // filter coefficients + complex double * cpxCoefs; // make the complex coefficients from dCoefs + int nBuf; // dimension of cBuf + int nTaps; // dimension of dSamples, cSamples, dCoefs and cpxCoefs + int decim_index; // used to count samples for decimation + complex double * cSamples; // storage for old samples + complex double * ptcSamp; // next available position in cSamples + complex double * cBuf; // auxillary buffer for interpolation +} ; + +struct quisk_dFilter { + double * dCoefs; // filter coefficients + complex double * cpxCoefs; // make the complex coefficients from dCoefs + int nBuf; // dimension of dBuf + int nTaps; // dimension of dSamples, cSamples, dCoefs and cpxCoefs + int decim_index; // used to count samples for decimation + double * dSamples; // storage for old samples + double * ptdSamp; // next available position in dSamples + double * dBuf; // auxillary buffer for interpolation +} ; + +struct quisk_cHB45Filter { // Complex half band decimate by 2 filter with 45 coefficients + complex double * cBuf; // auxillary buffer for interpolation + int nBuf; // dimension of cBuf + int toggle; + complex double samples[22]; + complex double center[11]; +} ; + +struct quisk_dHB45Filter { // Real half band decimate by 2 filter with 45 coefficients + double * dBuf; // auxillary buffer for interpolation + int nBuf; // dimension of dBuf + int toggle; + double samples[22]; + double center[11]; +} ; + +void quisk_filt_cInit(struct quisk_cFilter *, double *, int); +void quisk_filt_dInit(struct quisk_dFilter *, double *, int); +void quisk_filt_differInit(struct quisk_dFilter *, int); +void quisk_filt_tune(struct quisk_dFilter *, double, int); +complex double quisk_dC_out(double, struct quisk_dFilter *); +double quisk_dD_out(double, struct quisk_dFilter *); +int quisk_cInterpolate(complex double *, int, struct quisk_cFilter *, int); +int quisk_dInterpolate(double *, int, struct quisk_dFilter *, int); +int quisk_cDecimate(complex double *, int, struct quisk_cFilter *, int); +int quisk_cCDecimate(complex double *, int, struct quisk_cFilter *, int); +int quisk_dDecimate(double *, int, struct quisk_dFilter *, int); +int quisk_cInterpDecim(complex double *, int, struct quisk_cFilter *, int, int); +int quisk_cDecim2HB45(complex double *, int, struct quisk_cHB45Filter *); +int quisk_dInterp2HB45(double *, int, struct quisk_dHB45Filter *); +int quisk_cInterp2HB45(complex double *, int, struct quisk_cHB45Filter *); +int quisk_dFilter(double *, int, struct quisk_dFilter *); +int quisk_cFilter(complex double *, int, struct quisk_cFilter *); + +extern double quiskMicFilt48Coefs[325]; +extern double quiskMic5Filt48Coefs[424]; +extern double quiskMicFilt8Coefs[93]; +extern double quiskLpFilt48Coefs[186]; +extern double quiskFilt12_19Coefs[64]; +extern double quiskFilt185D3Coefs[189]; +extern double quiskFilt133D2Coefs[136]; +extern double quiskFilt167D3Coefs[174]; +extern double quiskFilt111D2Coefs[114]; +extern double quiskFilt53D1Coefs[55]; +extern double quiskFilt53D2Coefs[93]; +extern double quiskFilt144D3Coefs[147]; +extern double quiskFilt240D5Coefs[115]; +extern double quiskFilt240D5CoefsSharp[245]; +extern double quiskFilt48dec24Coefs[98]; +extern double quiskAudio24p6Coefs[36]; +extern double quiskAudio48p6Coefs[71]; +extern double quiskAudio96Coefs[11]; +extern double quiskAudio24p4Coefs[50]; +extern double quiskAudioFmHpCoefs[309]; +extern double quiskAudio24p3Coefs[100]; +extern double quiskFiltTx8kAudioB[168]; +extern double quiskFilt16dec8Coefs[62]; +extern double quiskFilt120s03[480]; +extern double quiskFiltI3D25Coefs[825]; +extern double quiskDgtFilt48Coefs[520]; +extern double quiskFilt300D5Coefs[125]; +extern double quiskFilt300D6Coefs[248]; +extern double quiskFilt240D4Coefs[100]; +extern double quiskDiff48Coefs[38]; diff --git a/filters.h b/filters.h new file mode 100644 index 0000000..957ef7f --- /dev/null +++ b/filters.h @@ -0,0 +1,1308 @@ +// Sample 48000 Hz, pass 1350, stop 1750, ripple 1 dB, atten 80 dB (coef_tx). Stop 0.036458. +double quiskMicFilt48Coefs[325] = { + 0.000070000950338734, 0.000060542525732639, 0.000084830915167248, 0.000113820318991206, 0.000147523658260692, + 0.000185727097247136, 0.000228017694264326, 0.000273714746744222, 0.000321902242967596, 0.000371355069437399, + 0.000420595492180569, 0.000467865412686539, 0.000511235564174107, 0.000548584697295435, 0.000577683697404093, + 0.000596168573595124, 0.000601725499419238, 0.000592142083607333, 0.000565459097743531, 0.000519884471131262, + 0.000454031497680723, 0.000367015380281216, 0.000258599355817496, 0.000128888056076112, -0.000021074826181109, +-0.000189585802186493, -0.000374389987374961, -0.000572207780852182, -0.000779359325040342, -0.000991335380833019, +-0.001203244280115714, -0.001409670281637694, -0.001605003575053789, -0.001783471817219780, -0.001939438753998784, +-0.002067470485014850, -0.002162628068669760, -0.002220597743425130, -0.002237960051673301, -0.002212264773625852, +-0.002142247186138010, -0.002027907322210780, -0.001870648667784210, -0.001673216059663119, -0.001439782952032160, +-0.001175849407968695, -0.000888138367181945, -0.000584400564491252, -0.000273350274681290, 0.000035800336978808, + 0.000333410168961052, 0.000609868204837889, 0.000855845198751289, 0.001062645743133108, 0.001222567778915907, + 0.001329178561171123, 0.001377619146269160, 0.001364836763234778, 0.001289791422827911, 0.001153569049793089, + 0.000959442606127223, 0.000712858984801055, 0.000421343499696318, 0.000094311703309987, -0.000257178812726900, +-0.000620740024523200, -0.000983067401076951, -0.001330378208545664, -0.001648868642029554, -0.001925231739749957, +-0.002147185427808426, -0.002303924955683100, -0.002386617358290471, -0.002388809260074332, -0.002306735130576522, +-0.002139640086196955, -0.001889862283127962, -0.001562946712603551, -0.001167542853751166, -0.000715240442693352, +-0.000220259485535331, 0.000300952307259904, 0.000830234179165372, 0.001348316435464076, 0.001835484518420602, + 0.002272286892056382, 0.002640275270224598, 0.002922747186895021, 0.003105429025669433, 0.003177121318055631, + 0.003130264761140124, 0.002961377569599093, 0.002671367231884301, 0.002265711108816848, 0.001754448044185524, + 0.001152016769339380, 0.000476936676175902, -0.000248718444031171, -0.000999948124398612, -0.001749629130166096, +-0.002469433330814752, -0.003130790556013717, -0.003705967375692800, -0.004169105103972317, -0.004497290920890501, +-0.004671516289830527, -0.004677586380416906, -0.004506861602367489, -0.004156859083959776, -0.003631621917386205, +-0.002941916481868308, -0.002105153002563826, -0.001145089028022731, -0.000091277027194512, 0.001021707212306502, + 0.002155280663061357, 0.003268024493993296, 0.004317048090841251, 0.005259445594087794, 0.006053837753595639, + 0.006661931665353417, 0.007050060676035087, 0.007190601958065676, 0.007063321534553406, 0.006656461303944376, + 0.005967650471713226, 0.005004501271125813, 0.003784934033421286, 0.002337151835923401, 0.000699290786282728, +-0.001081281658450751, -0.002948980083931280, -0.004841350286243770, -0.006690670765283031, -0.008425801537676034, +-0.009974215633219711, -0.011264158097802190, -0.012226858155753686, -0.012798749185527288, -0.012923592591893964, +-0.012554478278462186, -0.011655599268700838, -0.010203756484326235, -0.008189534541107277, -0.005618124746137401, +-0.002509708641553183, 0.001100549166042612, 0.005162968475167027, 0.009614243563845803, 0.014378746109223314, + 0.019370216567322070, 0.024493786616406266, 0.029648291853908371, 0.034728809072115958, 0.039629330964538544, + 0.044245520769433347, 0.048477462256357018, 0.052232311099492307, 0.055426788736225432, 0.057989428195401226, + 0.059862511141804443, 0.061003649799348830, 0.061386934931288717, 0.061003649799348830, 0.059862511141804443, + 0.057989428195401226, 0.055426788736225432, 0.052232311099492307, 0.048477462256357018, 0.044245520769433347, + 0.039629330964538544, 0.034728809072115958, 0.029648291853908371, 0.024493786616406266, 0.019370216567322070, + 0.014378746109223314, 0.009614243563845803, 0.005162968475167027, 0.001100549166042612, -0.002509708641553183, +-0.005618124746137401, -0.008189534541107277, -0.010203756484326235, -0.011655599268700838, -0.012554478278462186, +-0.012923592591893964, -0.012798749185527288, -0.012226858155753686, -0.011264158097802190, -0.009974215633219711, +-0.008425801537676034, -0.006690670765283031, -0.004841350286243770, -0.002948980083931280, -0.001081281658450751, + 0.000699290786282728, 0.002337151835923401, 0.003784934033421286, 0.005004501271125813, 0.005967650471713226, + 0.006656461303944376, 0.007063321534553406, 0.007190601958065676, 0.007050060676035087, 0.006661931665353417, + 0.006053837753595639, 0.005259445594087794, 0.004317048090841251, 0.003268024493993296, 0.002155280663061357, + 0.001021707212306502, -0.000091277027194512, -0.001145089028022731, -0.002105153002563826, -0.002941916481868308, +-0.003631621917386205, -0.004156859083959776, -0.004506861602367489, -0.004677586380416906, -0.004671516289830527, +-0.004497290920890501, -0.004169105103972317, -0.003705967375692800, -0.003130790556013717, -0.002469433330814752, +-0.001749629130166096, -0.000999948124398612, -0.000248718444031171, 0.000476936676175902, 0.001152016769339380, + 0.001754448044185524, 0.002265711108816848, 0.002671367231884301, 0.002961377569599093, 0.003130264761140124, + 0.003177121318055631, 0.003105429025669433, 0.002922747186895021, 0.002640275270224598, 0.002272286892056382, + 0.001835484518420602, 0.001348316435464076, 0.000830234179165372, 0.000300952307259904, -0.000220259485535331, +-0.000715240442693352, -0.001167542853751166, -0.001562946712603551, -0.001889862283127962, -0.002139640086196955, +-0.002306735130576522, -0.002388809260074332, -0.002386617358290471, -0.002303924955683100, -0.002147185427808426, +-0.001925231739749957, -0.001648868642029554, -0.001330378208545664, -0.000983067401076951, -0.000620740024523200, +-0.000257178812726900, 0.000094311703309987, 0.000421343499696318, 0.000712858984801055, 0.000959442606127223, + 0.001153569049793089, 0.001289791422827911, 0.001364836763234778, 0.001377619146269160, 0.001329178561171123, + 0.001222567778915907, 0.001062645743133108, 0.000855845198751289, 0.000609868204837889, 0.000333410168961052, + 0.000035800336978808, -0.000273350274681290, -0.000584400564491252, -0.000888138367181945, -0.001175849407968695, +-0.001439782952032160, -0.001673216059663119, -0.001870648667784210, -0.002027907322210780, -0.002142247186138010, +-0.002212264773625852, -0.002237960051673301, -0.002220597743425130, -0.002162628068669760, -0.002067470485014850, +-0.001939438753998784, -0.001783471817219780, -0.001605003575053789, -0.001409670281637694, -0.001203244280115714, +-0.000991335380833019, -0.000779359325040342, -0.000572207780852182, -0.000374389987374961, -0.000189585802186493, +-0.000021074826181109, 0.000128888056076112, 0.000258599355817496, 0.000367015380281216, 0.000454031497680723, + 0.000519884471131262, 0.000565459097743531, 0.000592142083607333, 0.000601725499419238, 0.000596168573595124, + 0.000577683697404093, 0.000548584697295435, 0.000511235564174107, 0.000467865412686539, 0.000420595492180569, + 0.000371355069437399, 0.000321902242967596, 0.000273714746744222, 0.000228017694264326, 0.000185727097247136, + 0.000147523658260692, 0.000113820318991206, 0.000084830915167248, 0.000060542525732639, 0.000070000950338734} ; + +// Rate 8000, pass 1350, stop 1700, ripple 0.2 dB, Atten 100 dB, taps 93 (tune 1650). Stop 0.2125 +double quiskMicFilt8Coefs[93] = { -0.000048361334397216, -0.000162172832480051, -0.000233937595698212, +-0.000006327103457766, 0.000686150429731403, 0.001555572669911020, 0.001852235322638049, 0.001010518676959113, +-0.000533703037637750, -0.001395527281294679, -0.000547805689731835, 0.001291819787342756, 0.002005877810728077, + 0.000349773034952121, -0.002188174349636707, -0.002546652809598079, 0.000326796204088075, 0.003522458156729808, + 0.002929815624806958, -0.001643263026946036, -0.005188443834846179, -0.002794087489903680, 0.003828812643532208, + 0.006979098861533383, 0.001700757602284474, -0.007013801827881641, -0.008485813922195549, 0.000867966129209818, + 0.011177153889901327, 0.009069558799223736, -0.005488279673121778, -0.016106658511052186, -0.007822070151870978, + 0.012835116756370880, 0.021388877055372072, 0.003393318192541889, -0.023987911353715568, -0.026461891098409809, + 0.006724804206592470, 0.041707244257452522, 0.030690541453250592, -0.029955120082834141, -0.077739106243522968, +-0.033499374091835024, 0.116943716609774510, 0.290907261177841880, 0.367819184749133500, 0.290907261177841880, + 0.116943716609774510, -0.033499374091835024, -0.077739106243522968, -0.029955120082834141, 0.030690541453250592, + 0.041707244257452522, 0.006724804206592470, -0.026461891098409809, -0.023987911353715568, 0.003393318192541889, + 0.021388877055372072, 0.012835116756370880, -0.007822070151870978, -0.016106658511052186, -0.005488279673121778, + 0.009069558799223736, 0.011177153889901327, 0.000867966129209818, -0.008485813922195549, -0.007013801827881641, + 0.001700757602284474, 0.006979098861533383, 0.003828812643532208, -0.002794087489903680, -0.005188443834846179, +-0.001643263026946036, 0.002929815624806958, 0.003522458156729808, 0.000326796204088075, -0.002546652809598079, +-0.002188174349636707, 0.000349773034952121, 0.002005877810728077, 0.001291819787342756, -0.000547805689731835, +-0.001395527281294679, -0.000533703037637750, 0.001010518676959113, 0.001852235322638049, 0.001555572669911020, + 0.000686150429731403, -0.000006327103457766, -0.000233937595698212, -0.000162172832480051, -0.000048361334397216} ; + +// Rate 48000, pass 3000, stop 4000, ripple 0.2 dB, atten 100 dB, taps 186. Pass 0.0625. Stop .083333. +double quiskLpFilt48Coefs[186] = { -0.000013634802929206, -0.000025833607451468, -0.000045098917291799, +-0.000067397454184632, -0.000087934612849518, -0.000099336288085176, -0.000092164590568194, -0.000056185338095760, + 0.000017628479128830, 0.000134695138796130, 0.000294364688920149, 0.000487930816326543, 0.000697781352809774, + 0.000898146218636447, 0.001057686434085801, 0.001143917302878051, 0.001128977827853932, 0.000995827794007123, + 0.000743730207286422, 0.000391756255003027, -0.000020838483133667, -0.000438064704829056, -0.000794512795634084, +-0.001025788840201078, -0.001080548087342285, -0.000932013104946299, -0.000586606665219995, -0.000087502631308000, + 0.000488407444073969, 0.001040518103766967, 0.001460541797297978, 0.001652270668043200, 0.001551661149061061, + 0.001143287935426198, 0.000469406269084863, -0.000371161499314815, -0.001235417370813682, -0.001958835103416428, +-0.002385048283250022, -0.002397529299276367, -0.001947179493277411, -0.001069838763968274, 0.000110976961796973, + 0.001398395929344110, 0.002552691150332272, 0.003333738458450593, 0.003548332671239537, 0.003093481207411224, + 0.001986606612550283, 0.000375208969545557, -0.001478173505667034, -0.003235390501589682, -0.004541286966345233, +-0.005091921692691575, -0.004699905483508720, -0.003343628882412480, -0.001188973425908869, 0.001423632771579898, + 0.004027852785840577, 0.006109889449352044, 0.007204411806930103, 0.006990964143613923, 0.005372067993842730, + 0.002515924626355224, -0.001148191299404085, -0.004985775758010277, -0.008258611238961837, -0.010257070307501979, +-0.010440987284724979, -0.008563091952746314, -0.004749779955204285, 0.000480411506971036, 0.006270496250094757, + 0.011548336869209398, 0.015203957673842774, 0.016294017929596276, 0.014239284159958419, 0.008979207551576943, + 0.001052260094328930, -0.008418875271295619, -0.017843305879886966, -0.025368239712008800, -0.029148572862344568, +-0.027640215600942798, -0.019871118305655480, -0.005644876301707345, 0.014359687674705773, 0.038614566536650477, + 0.064896650800270142, 0.090551963489257925, 0.112834114097579010, 0.129268306000616030, 0.137987044444314420, + 0.137987044444314420, 0.129268306000616030, 0.112834114097579010, 0.090551963489257925, 0.064896650800270142, + 0.038614566536650477, 0.014359687674705773, -0.005644876301707345, -0.019871118305655480, -0.027640215600942798, +-0.029148572862344568, -0.025368239712008800, -0.017843305879886966, -0.008418875271295619, 0.001052260094328930, + 0.008979207551576943, 0.014239284159958419, 0.016294017929596276, 0.015203957673842774, 0.011548336869209398, + 0.006270496250094757, 0.000480411506971036, -0.004749779955204285, -0.008563091952746314, -0.010440987284724979, +-0.010257070307501979, -0.008258611238961837, -0.004985775758010277, -0.001148191299404085, 0.002515924626355224, + 0.005372067993842730, 0.006990964143613923, 0.007204411806930103, 0.006109889449352044, 0.004027852785840577, + 0.001423632771579898, -0.001188973425908869, -0.003343628882412480, -0.004699905483508720, -0.005091921692691575, +-0.004541286966345233, -0.003235390501589682, -0.001478173505667034, 0.000375208969545557, 0.001986606612550283, + 0.003093481207411224, 0.003548332671239537, 0.003333738458450593, 0.002552691150332272, 0.001398395929344110, + 0.000110976961796973, -0.001069838763968274, -0.001947179493277411, -0.002397529299276367, -0.002385048283250022, +-0.001958835103416428, -0.001235417370813682, -0.000371161499314815, 0.000469406269084863, 0.001143287935426198, + 0.001551661149061061, 0.001652270668043200, 0.001460541797297978, 0.001040518103766967, 0.000488407444073969, +-0.000087502631308000, -0.000586606665219995, -0.000932013104946299, -0.001080548087342285, -0.001025788840201078, +-0.000794512795634084, -0.000438064704829056, -0.000020838483133667, 0.000391756255003027, 0.000743730207286422, + 0.000995827794007123, 0.001128977827853932, 0.001143917302878051, 0.001057686434085801, 0.000898146218636447, + 0.000697781352809774, 0.000487930816326543, 0.000294364688920149, 0.000134695138796130, 0.000017628479128830, +-0.000056185338095760, -0.000092164590568194, -0.000099336288085176, -0.000087934612849518, -0.000067397454184632, +-0.000045098917291799, -0.000025833607451468, -0.000013634802929206 } ; + +// Sample 1 Hz, pass 0.125, stop 0.192, ripple 0.1 dB, atten 100 dB, taps 64. Stop 0.192. +double quiskFilt12_19Coefs[64] = { 0.000043810351462319, 0.000138863869398690, 0.000206835545025070, + 0.000047730531591317, -0.000514727559529336, -0.001353635480056038, -0.001881242409327435, -0.001336759882352925, + 0.000480762137654505, 0.002595795305500315, 0.003157538223935716, 0.000883738175457450, -0.003304786290465149, +-0.006137600456523736, -0.004232173904239833, 0.002529546544381186, 0.009496143474996962, 0.009949625572871518, + 0.000962602913419665, -0.012260655539427043, -0.018443416827456323, -0.008986096201165510, 0.012737225552167929, + 0.030249086308101754, 0.024812387650292519, -0.007810917817793795, -0.047781081559953899, -0.059071807532626572, +-0.012800932712041477, 0.089119808681493592, 0.208124268210933110, 0.287998295491409540, 0.287998295491409540, + 0.208124268210933110, 0.089119808681493592, -0.012800932712041477, -0.059071807532626572, -0.047781081559953899, +-0.007810917817793795, 0.024812387650292519, 0.030249086308101754, 0.012737225552167929, -0.008986096201165510, +-0.018443416827456323, -0.012260655539427043, 0.000962602913419665, 0.009949625572871518, 0.009496143474996962, + 0.002529546544381186, -0.004232173904239833, -0.006137600456523736, -0.003304786290465149, 0.000883738175457450, + 0.003157538223935716, 0.002595795305500315, 0.000480762137654505, -0.001336759882352925, -0.001881242409327435, +-0.001353635480056038, -0.000514727559529336, 0.000047730531591317, 0.000206835545025070, 0.000138863869398690, + 0.000043810351462319} ; + +// Sample 185185 Hz, pass 20000, stop 24000, ripple 0.1 dB, atten 100 dB. For SDR-IQ. Stop 0.12960. +double quiskFilt185D3Coefs[189] = { 0.000012775222963944, 0.000016983455496025, 0.000005695459401072, -0.000043370392322925, +-0.000147289810253534, -0.000306853192747712, -0.000495824111042255, -0.000659912449094600, -0.000730624820807555, +-0.000652527190094691, -0.000414108542407609, -0.000067101230547892, 0.000279651977919746, 0.000496266897694305, + 0.000487147757737141, 0.000242333355062937, -0.000142550724009829, -0.000494575786766818, -0.000635523916496215, +-0.000469056286907829, -0.000040928295181758, 0.000463663137093341, 0.000794834769348999, 0.000757740301889996, + 0.000321720777800376, -0.000340193393344642, -0.000912913559618510, -0.001084751128773843, -0.000711940589185640, + 0.000082299572584560, 0.000936277365105903, 0.001408757213795883, 0.001201831453599291, 0.000337664985017852, +-0.000811409064230295, -0.001675111110230421, -0.001762493246689817, -0.000932484695478804, 0.000487612811028567, + 0.001818232361534366, 0.002345552396132496, 0.001696550268334318, 0.000077777468070599, -0.001766041325684469, +-0.002884900730916659, -0.002604595228251389, -0.000916585198089093, 0.001441898300187086, 0.003295669370079313, + 0.003607178283399026, 0.002043134080945332, -0.000770834928466311, -0.003477680777975640, -0.004630423337101654, +-0.003451243412347569, -0.000316897327995463, 0.003317404994207552, 0.005575542061462892, 0.005112123782225341, + 0.001883844314927795, -0.002688463188924373, -0.006318001179294125, -0.006973801349612467, -0.003986753493372796, + 0.001448254061321833, 0.006704745658002290, 0.008962905591378809, 0.006685267872330669, 0.000575241526335665, +-0.006544512593532594, -0.010988781408054678, -0.010065358566522303, -0.003619687890287124, 0.005579382505096557, + 0.012949729773207512, 0.014296503321222344, 0.008083093875296470, -0.003403672893464083, -0.014738780224596465, +-0.019777086918256612, -0.014784814209033215, -0.000774387161930971, 0.016257202240672342, 0.027605140528859175, + 0.025878721397717495, 0.009050451134125328, -0.017412070606886515, -0.041603859087594751, -0.049367067487597505, +-0.030106928037341840, 0.018134551394911973, 0.086605309563255489, 0.157907419743824910, 0.211651319263293920, + 0.231619508265925700, 0.211651319263293920, 0.157907419743824910, 0.086605309563255489, 0.018134551394911973, +-0.030106928037341840, -0.049367067487597505, -0.041603859087594751, -0.017412070606886515, 0.009050451134125328, + 0.025878721397717495, 0.027605140528859175, 0.016257202240672342, -0.000774387161930971, -0.014784814209033215, +-0.019777086918256612, -0.014738780224596465, -0.003403672893464083, 0.008083093875296470, 0.014296503321222344, + 0.012949729773207512, 0.005579382505096557, -0.003619687890287124, -0.010065358566522303, -0.010988781408054678, +-0.006544512593532594, 0.000575241526335665, 0.006685267872330669, 0.008962905591378809, 0.006704745658002290, + 0.001448254061321833, -0.003986753493372796, -0.006973801349612467, -0.006318001179294125, -0.002688463188924373, + 0.001883844314927795, 0.005112123782225341, 0.005575542061462892, 0.003317404994207552, -0.000316897327995463, +-0.003451243412347569, -0.004630423337101654, -0.003477680777975640, -0.000770834928466311, 0.002043134080945332, + 0.003607178283399026, 0.003295669370079313, 0.001441898300187086, -0.000916585198089093, -0.002604595228251389, +-0.002884900730916659, -0.001766041325684469, 0.000077777468070599, 0.001696550268334318, 0.002345552396132496, + 0.001818232361534366, 0.000487612811028567, -0.000932484695478804, -0.001762493246689817, -0.001675111110230421, +-0.000811409064230295, 0.000337664985017852, 0.001201831453599291, 0.001408757213795883, 0.000936277365105903, + 0.000082299572584560, -0.000711940589185640, -0.001084751128773843, -0.000912913559618510, -0.000340193393344642, + 0.000321720777800376, 0.000757740301889996, 0.000794834769348999, 0.000463663137093341, -0.000040928295181758, +-0.000469056286907829, -0.000635523916496215, -0.000494575786766818, -0.000142550724009829, 0.000242333355062937, + 0.000487147757737141, 0.000496266897694305, 0.000279651977919746, -0.000067101230547892, -0.000414108542407609, +-0.000652527190094691, -0.000730624820807555, -0.000659912449094600, -0.000495824111042255, -0.000306853192747712, +-0.000147289810253534, -0.000043370392322925, 0.000005695459401072, 0.000016983455496025, 0.000012775222963944} ; + +// Sample 185185 Hz, pass 15000, stop 23900, ripple 0.1 dB, atten 100 dB. For SDR-IQ. Stop 0.129060. +double quiskFilt185D3XCoefs[88] = { -0.000026016801458962, -0.000079343977811083, -0.000167897725115826, -0.000272158478205181, +-0.000343785933253942, -0.000310796788289393, -0.000102311891399797, 0.000311300138822939, 0.000877228005105512, 0.001440583394256035, + 0.001767836749778265, 0.001621580974541500, 0.000871040137123456, -0.000402149294308734, -0.001857385353819294, -0.002952994474321905, +-0.003118971956742465, -0.002002468636632821, 0.000304795748571584, 0.003168465665729574, 0.005527734084410955, 0.006236194024480606, + 0.004538088065486723, 0.000504287301538830, -0.004776061900406583, -0.009396166048199123, -0.011236677659113514, -0.008814076686757433, +-0.002068388398230007, 0.007257051941983628, 0.015896603006282241, 0.020037182698727726, 0.016744338396284194, 0.005368795820044257, +-0.011616528320034477, -0.028774969855905743, -0.038977373476370786, -0.035506674315576942, -0.014373032297997279, 0.023936198172814224, + 0.073933459676699551, 0.126134168736811850, 0.169387884611201690, 0.193878581402395330, 0.193878581402395330, 0.169387884611201690, + 0.126134168736811850, 0.073933459676699551, 0.023936198172814224, -0.014373032297997279, -0.035506674315576942, -0.038977373476370786, +-0.028774969855905743, -0.011616528320034477, 0.005368795820044257, 0.016744338396284194, 0.020037182698727726, 0.015896603006282241, + 0.007257051941983628, -0.002068388398230007, -0.008814076686757433, -0.011236677659113514, -0.009396166048199123, -0.004776061900406583, + 0.000504287301538830, 0.004538088065486723, 0.006236194024480606, 0.005527734084410955, 0.003168465665729574, 0.000304795748571584, +-0.002002468636632821, -0.003118971956742465, -0.002952994474321905, -0.001857385353819294, -0.000402149294308734, 0.000871040137123456, + 0.001621580974541500, 0.001767836749778265, 0.001440583394256035, 0.000877228005105512, 0.000311300138822939, -0.000102311891399797, +-0.000310796788289393, -0.000343785933253942, -0.000272158478205181, -0.000167897725115826, -0.000079343977811083, -0.000026016801458962} ; + +// Sample 185185 Hz, pass 10000, stop 12000, ripple 0.5 dB, atten 100 dB. For SDR-IQ. Stop 0.064800. +double quiskFilt185D7Coefs[325] = { 0.000011585872492535, 0.000021202775655724, 0.000038515581433427, 0.000062668164659740, + 0.000093940890103130, 0.000131718743859808, 0.000174225065604974, 0.000218342986026828, 0.000259582942086064, 0.000292202414812531, + 0.000309501208323850, 0.000304347853118504, 0.000269885084834102, 0.000200329336288352, 0.000091873319388286, +-0.000056460226867932, -0.000242180306367212, -0.000458826972767189, -0.000695972007921572, -0.000939539009494425, +-0.001172660992563646, -0.001376927020458812, -0.001534012395041686, -0.001627510712907692, -0.001644810967208332, +-0.001578811291153747, -0.001429231737210363, -0.001203341468259860, -0.000915956952592136, -0.000588604281778632, +-0.000247881153416102, 0.000076883069806609, 0.000356496498766536, 0.000564970346134735, 0.000682503931650803, + 0.000697978266499694, 0.000610677107413264, 0.000430929848100204, 0.000179551442826917, -0.000113968106450125, +-0.000414397628381283, -0.000684546545073360, -0.000889508690226669, -0.001000889233610915, -0.001000492619169160, +-0.000883003638604998, -0.000657261341613736, -0.000345878903792753, 0.000016835572177804, 0.000388523086839818, + 0.000723605124037493, 0.000978796711705906, 0.001118627280774081, 0.001120302957105695, 0.000977241071844119, + 0.000700761818102867, 0.000319615424985736, -0.000122724655145581, -0.000572787713987942, -0.000973362887973448, +-0.001270584863164685, -0.001420984914880352, -0.001397605040893653, -0.001194338356762529, -0.000827834100503333, +-0.000336591061237865, 0.000222813863757337, 0.000782022394777827, 0.001269065085645792, 0.001617440270226089, + 0.001774989493019493, 0.001711406052073067, 0.001423329284260602, 0.000936230433482062, 0.000302669877122975, +-0.000403051367712102, -0.001093347917242359, -0.001677989133371222, -0.002075657051446509, -0.002224973164684223, +-0.002093532018815395, -0.001683654864926617, -0.001033944903045330, -0.000216202715201031, 0.000672136237235640, + 0.001519308622149400, 0.002212998394841567, 0.002654922443391980, 0.002774368358702789, 0.002538866483308035, + 0.001960451088476113, 0.001096459360528461, 0.000044471086033573, -0.001068263405090781, -0.002099826730828226, +-0.002911349453455988, -0.003385273847682783, -0.003441820002543952, -0.003051407043926876, -0.002241202920048492, +-0.001094628195429446, 0.000256492779785520, 0.001646511036377237, 0.002895722994780784, 0.003833041920244146, + 0.004318777856326995, 0.004264570929820472, 0.003647733587396600, 0.002517835568167883, 0.000994251746281646, +-0.000745501220005664, -0.002485634335945939, -0.003998039828567652, -0.005071301821004867, -0.005539183731069824, +-0.005304874120435773, -0.004357624572350711, -0.002779195755674417, -0.000738704575887045, 0.001524133036686280, + 0.003725875479734138, 0.005573480900438330, 0.006801730588660005, 0.007209350069135231, 0.006689128326667765, + 0.005247829163413888, 0.003012735394874103, 0.000223174064082399, -0.002792831962376837, -0.005654763344137692, +-0.007974895792849587, -0.009407569536250765, -0.009696510630814705, -0.008714169676261001, -0.006487651574925092, +-0.003207133255888027, 0.000785450700374519, 0.005027410607157849, 0.008983570118297410, 0.012107159427564776, + 0.013906909894651421, 0.014012809617679676, 0.012232695276105182, 0.008592471450916649, 0.003354235296550607, +-0.002991194159451697, -0.009758117856276788, -0.016123999816583842, -0.021206724919405923, -0.024153695524852702, +-0.024233748891356845, -0.020922161341036132, -0.013969353698339257, -0.003445298078014795, 0.010246061695767921, + 0.026385065799221116, 0.043986586663902316, 0.061878202104145755, 0.078799201858789497, 0.093511010803744687, + 0.104908248570820360, 0.112119405527806140, 0.114587041362813750, 0.112119405527806140, 0.104908248570820360, + 0.093511010803744687, 0.078799201858789497, 0.061878202104145755, 0.043986586663902316, 0.026385065799221116, + 0.010246061695767921, -0.003445298078014795, -0.013969353698339257, -0.020922161341036132, -0.024233748891356845, +-0.024153695524852702, -0.021206724919405923, -0.016123999816583842, -0.009758117856276788, -0.002991194159451697, + 0.003354235296550607, 0.008592471450916649, 0.012232695276105182, 0.014012809617679676, 0.013906909894651421, + 0.012107159427564776, 0.008983570118297410, 0.005027410607157849, 0.000785450700374519, -0.003207133255888027, +-0.006487651574925092, -0.008714169676261001, -0.009696510630814705, -0.009407569536250765, -0.007974895792849587, +-0.005654763344137692, -0.002792831962376837, 0.000223174064082399, 0.003012735394874103, 0.005247829163413888, + 0.006689128326667765, 0.007209350069135231, 0.006801730588660005, 0.005573480900438330, 0.003725875479734138, + 0.001524133036686280, -0.000738704575887045, -0.002779195755674417, -0.004357624572350711, -0.005304874120435773, +-0.005539183731069824, -0.005071301821004867, -0.003998039828567652, -0.002485634335945939, -0.000745501220005664, + 0.000994251746281646, 0.002517835568167883, 0.003647733587396600, 0.004264570929820472, 0.004318777856326995, + 0.003833041920244146, 0.002895722994780784, 0.001646511036377237, 0.000256492779785520, -0.001094628195429446, +-0.002241202920048492, -0.003051407043926876, -0.003441820002543952, -0.003385273847682783, -0.002911349453455988, +-0.002099826730828226, -0.001068263405090781, 0.000044471086033573, 0.001096459360528461, 0.001960451088476113, + 0.002538866483308035, 0.002774368358702789, 0.002654922443391980, 0.002212998394841567, 0.001519308622149400, + 0.000672136237235640, -0.000216202715201031, -0.001033944903045330, -0.001683654864926617, -0.002093532018815395, +-0.002224973164684223, -0.002075657051446509, -0.001677989133371222, -0.001093347917242359, -0.000403051367712102, + 0.000302669877122975, 0.000936230433482062, 0.001423329284260602, 0.001711406052073067, 0.001774989493019493, + 0.001617440270226089, 0.001269065085645792, 0.000782022394777827, 0.000222813863757337, -0.000336591061237865, +-0.000827834100503333, -0.001194338356762529, -0.001397605040893653, -0.001420984914880352, -0.001270584863164685, +-0.000973362887973448, -0.000572787713987942, -0.000122724655145581, 0.000319615424985736, 0.000700761818102867, + 0.000977241071844119, 0.001120302957105695, 0.001118627280774081, 0.000978796711705906, 0.000723605124037493, + 0.000388523086839818, 0.000016835572177804, -0.000345878903792753, -0.000657261341613736, -0.000883003638604998, +-0.001000492619169160, -0.001000889233610915, -0.000889508690226669, -0.000684546545073360, -0.000414397628381283, +-0.000113968106450125, 0.000179551442826917, 0.000430929848100204, 0.000610677107413264, 0.000697978266499694, + 0.000682503931650803, 0.000564970346134735, 0.000356496498766536, 0.000076883069806609, -0.000247881153416102, +-0.000588604281778632, -0.000915956952592136, -0.001203341468259860, -0.001429231737210363, -0.001578811291153747, +-0.001644810967208332, -0.001627510712907692, -0.001534012395041686, -0.001376927020458812, -0.001172660992563646, +-0.000939539009494425, -0.000695972007921572, -0.000458826972767189, -0.000242180306367212, -0.000056460226867932, + 0.000091873319388286, 0.000200329336288352, 0.000269885084834102, 0.000304347853118504, 0.000309501208323850, + 0.000292202414812531, 0.000259582942086064, 0.000218342986026828, 0.000174225065604974, 0.000131718743859808, + 0.000093940890103130, 0.000062668164659740, 0.000038515581433427, 0.000021202775655724, 0.000011585872492535} ; + +// Sample 133333 Hz, pass 20000, stop 24000, ripple 0.1 dB, atten 100 dB. For SDR-IQ. Stop 0.18000. +double quiskFilt133D2Coefs[136] = { 0.000017194140195307, 0.000013031580406032, -0.000070982819622986, -0.000297799318127961, +-0.000645057684247744, -0.000951723373793975, -0.000976667009837673, -0.000579840699091521, 0.000098129751533427, + 0.000644977424009104, 0.000648591932388761, 0.000063466245825653, -0.000655952785922865, -0.000858530950797784, +-0.000263313568128542, 0.000697280609267464, 0.001152025473909408, 0.000556416972642962, -0.000702600426682260, +-0.001503408593721275, -0.000962293487226091, 0.000635424413378249, 0.001893865760533597, 0.001494654948692980, +-0.000464212878209573, -0.002304129661798076, -0.002165029121341371, 0.000156986911460916, 0.002712310672598236, + 0.002984071948396753, 0.000321449210044334, -0.003091821762536665, -0.003961473137247651, -0.001010870707262950, + 0.003410242877909513, 0.005106330989341096, 0.001957278141537391, -0.003628441800145526, -0.006428886895772534, +-0.003216116999687284, 0.003699894436571663, 0.007945926308831793, 0.004862613142073883, -0.003562107867854992, +-0.009680253224586733, -0.006996529395398682, 0.003139846233471389, 0.011684868441375082, 0.009783362061267441, +-0.002313232717655680, -0.014049355073607583, -0.013495163730537300, 0.000895100102567855, 0.016959635420547809, + 0.018648538187471329, 0.001461901498462892, -0.020821848411902603, -0.026360133080398199, -0.005525410959722330, + 0.026648719554864163, 0.039549486288596822, 0.013454287175328376, -0.037723817929648518, -0.068973339495709857, +-0.034792487725714472, 0.073207163331834649, 0.211614628197181350, 0.308128392192117740, 0.308128392192117740, + 0.211614628197181350, 0.073207163331834649, -0.034792487725714472, -0.068973339495709857, -0.037723817929648518, + 0.013454287175328376, 0.039549486288596822, 0.026648719554864163, -0.005525410959722330, -0.026360133080398199, +-0.020821848411902603, 0.001461901498462892, 0.018648538187471329, 0.016959635420547809, 0.000895100102567855, +-0.013495163730537300, -0.014049355073607583, -0.002313232717655680, 0.009783362061267441, 0.011684868441375082, + 0.003139846233471389, -0.006996529395398682, -0.009680253224586733, -0.003562107867854992, 0.004862613142073883, + 0.007945926308831793, 0.003699894436571663, -0.003216116999687284, -0.006428886895772534, -0.003628441800145526, + 0.001957278141537391, 0.005106330989341096, 0.003410242877909513, -0.001010870707262950, -0.003961473137247651, +-0.003091821762536665, 0.000321449210044334, 0.002984071948396753, 0.002712310672598236, 0.000156986911460916, +-0.002165029121341371, -0.002304129661798076, -0.000464212878209573, 0.001494654948692980, 0.001893865760533597, + 0.000635424413378249, -0.000962293487226091, -0.001503408593721275, -0.000702600426682260, 0.000556416972642962, + 0.001152025473909408, 0.000697280609267464, -0.000263313568128542, -0.000858530950797784, -0.000655952785922865, + 0.000063466245825653, 0.000648591932388761, 0.000644977424009104, 0.000098129751533427, -0.000579840699091521, +-0.000976667009837673, -0.000951723373793975, -0.000645057684247744, -0.000297799318127961, -0.000070982819622986, + 0.000013031580406032, 0.000017194140195307 } ; + +// Sample 133333 Hz, pass 10000, stop 12000, ripple 0.5 dB, atten 100 dB. For SDR-IQ. Stop 0.09000. +double quiskFilt133D5Coefs[235] = { 0.000017189993342429, 0.000044738485582654, 0.000095017992345093, 0.000171520768649681, + 0.000274754514100238, 0.000398861594001340, 0.000529984999270100, 0.000646117397525726, 0.000718766415753168, + 0.000716925008973219, 0.000612844047432097, 0.000388876459995003, 0.000043995168792169, -0.000401638582561071, +-0.000905542666180542, -0.001406385530252347, -0.001832160018119073, -0.002112253709578303, -0.002191408150519588, +-0.002042816719676478, -0.001677284024812651, -0.001145752378681760, -0.000533679761891523, 0.000052653624322135, + 0.000505907329874479, 0.000739851765907619, 0.000709229648947976, 0.000422523833395244, -0.000056020452981720, +-0.000616720304461352, -0.001125962440173316, -0.001454995787757606, -0.001509950907384681, -0.001256131267758628, +-0.000730326866325590, -0.000037158838317771, 0.000671036130692201, 0.001227356473019533, 0.001489601586267346, + 0.001376632327061763, 0.000892728515088217, 0.000132987785167989, -0.000733102122237588, -0.001497662325285410, +-0.001962695382200209, -0.001989694482613183, -0.001538410312914189, -0.000684278577781320, 0.000391518648789513, + 0.001439060262231111, 0.002196666221903917, 0.002455206790769118, 0.002114248789295101, 0.001215358226109379, +-0.000057550388771950, -0.001413462921977421, -0.002519018104662746, -0.003078793094584098, -0.002912162814601297, +-0.002007172281996295, -0.000536145063816962, 0.001173803706907478, 0.002711347438868158, 0.003677986766436793, + 0.003789135200490261, 0.002954134059121999, 0.001313489128052297, -0.000779246120784695, -0.002830527021932420, +-0.004320552674014772, -0.004832115109577898, -0.004163289510814260, -0.002394421340991988, 0.000110859904206459, + 0.002775804501582138, 0.004939612978506710, 0.006017429310863970, 0.005653427023669567, 0.003828044237650523, + 0.000888978423738288, -0.002507663812902237, -0.005538235553289072, -0.007405523728182901, -0.007541454609909858, +-0.005768568026570733, -0.002376693049722654, 0.001911054897098118, 0.006083411898142144, 0.009070202108022866, + 0.010005711453736786, 0.008463803744478828, 0.004604287191332431, -0.000813142277372015, -0.006562286180605113, +-0.011211419856533258, -0.013459517260663504, -0.012472773254562211, -0.008139991258903686, -0.001177765649827518, + 0.006953975184466299, 0.014324290436841271, 0.018945676347138025, 0.019248150626647365, 0.014509895663648657, + 0.005142708852136439, -0.007245145517153711, -0.020042796993989549, -0.030067767509750015, -0.034161762145356343, +-0.029838715290132833, -0.015854346901954634, 0.007424778279719944, 0.037939728171909451, 0.072177533053372794, + 0.105698112853527980, 0.133859311332239410, 0.152607173067114270, 0.159182344236760560, 0.152607173067114270, + 0.133859311332239410, 0.105698112853527980, 0.072177533053372794, 0.037939728171909451, 0.007424778279719944, +-0.015854346901954634, -0.029838715290132833, -0.034161762145356343, -0.030067767509750015, -0.020042796993989549, +-0.007245145517153711, 0.005142708852136439, 0.014509895663648657, 0.019248150626647365, 0.018945676347138025, + 0.014324290436841271, 0.006953975184466299, -0.001177765649827518, -0.008139991258903686, -0.012472773254562211, +-0.013459517260663504, -0.011211419856533258, -0.006562286180605113, -0.000813142277372015, 0.004604287191332431, + 0.008463803744478828, 0.010005711453736786, 0.009070202108022866, 0.006083411898142144, 0.001911054897098118, +-0.002376693049722654, -0.005768568026570733, -0.007541454609909858, -0.007405523728182901, -0.005538235553289072, +-0.002507663812902237, 0.000888978423738288, 0.003828044237650523, 0.005653427023669567, 0.006017429310863970, + 0.004939612978506710, 0.002775804501582138, 0.000110859904206459, -0.002394421340991988, -0.004163289510814260, +-0.004832115109577898, -0.004320552674014772, -0.002830527021932420, -0.000779246120784695, 0.001313489128052297, + 0.002954134059121999, 0.003789135200490261, 0.003677986766436793, 0.002711347438868158, 0.001173803706907478, +-0.000536145063816962, -0.002007172281996295, -0.002912162814601297, -0.003078793094584098, -0.002519018104662746, +-0.001413462921977421, -0.000057550388771950, 0.001215358226109379, 0.002114248789295101, 0.002455206790769118, + 0.002196666221903917, 0.001439060262231111, 0.000391518648789513, -0.000684278577781320, -0.001538410312914189, +-0.001989694482613183, -0.001962695382200209, -0.001497662325285410, -0.000733102122237588, 0.000132987785167989, + 0.000892728515088217, 0.001376632327061763, 0.001489601586267346, 0.001227356473019533, 0.000671036130692201, +-0.000037158838317771, -0.000730326866325590, -0.001256131267758628, -0.001509950907384681, -0.001454995787757606, +-0.001125962440173316, -0.000616720304461352, -0.000056020452981720, 0.000422523833395244, 0.000709229648947976, + 0.000739851765907619, 0.000505907329874479, 0.000052653624322135, -0.000533679761891523, -0.001145752378681760, +-0.001677284024812651, -0.002042816719676478, -0.002191408150519588, -0.002112253709578303, -0.001832160018119073, +-0.001406385530252347, -0.000905542666180542, -0.000401638582561071, 0.000043995168792169, 0.000388876459995003, + 0.000612844047432097, 0.000716925008973219, 0.000718766415753168, 0.000646117397525726, 0.000529984999270100, + 0.000398861594001340, 0.000274754514100238, 0.000171520768649681, 0.000095017992345093, 0.000044738485582654, + 0.000017189993342429} ; + +// Sample 111111 Hz, pass 20000, stop 24000, ripple 0.1 dB, atten 100 dB. For SDR-IQ. Stop 0.21600. +double quiskFilt111D2Coefs[114] = { 0.000039619752517087, 0.000089439819783633, 0.000017353012666121, -0.000334808345968833, +-0.000892257134326630, -0.001221700854092078, -0.000859742027523152, 0.000086110163390211, 0.000794431841612731, + 0.000492910288706401, -0.000542730604541516, -0.001057738392194920, -0.000232619264909279, 0.001101735902638522, + 0.001208303693873524, -0.000351788978766190, -0.001781186751638964, -0.001038581835235810, 0.001330430232082531, + 0.002355496074120870, 0.000292615997120782, -0.002623102550864884, -0.002462643018753817, 0.001194053806308097, + 0.003912675164153385, 0.001670607202119238, -0.003361036335001390, -0.004638914091859385, 0.000363767953875138, + 0.005797757906125993, 0.004088942448683545, -0.003689094083671128, -0.007701448612726584, -0.001601464053307583, + 0.007864313939600763, 0.007963248868264130, -0.003156323310881499, -0.011852746523309575, -0.005390083465132761, + 0.009915212572859983, 0.014053676129843581, -0.000996935620086296, -0.017619302590712586, -0.012439281622203873, + 0.011722997082370391, 0.024364733312011930, 0.004596957308944789, -0.026911691600293927, -0.027288530345555617, + 0.013072844783675014, 0.047281998734662697, 0.021354076339639792, -0.051099079289920336, -0.080397920778604998, + 0.013794073914731197, 0.205298935938689170, 0.362799183591728360, 0.362799183591728360, 0.205298935938689170, + 0.013794073914731197, -0.080397920778604998, -0.051099079289920336, 0.021354076339639792, 0.047281998734662697, + 0.013072844783675014, -0.027288530345555617, -0.026911691600293927, 0.004596957308944789, 0.024364733312011930, + 0.011722997082370391, -0.012439281622203873, -0.017619302590712586, -0.000996935620086296, 0.014053676129843581, + 0.009915212572859983, -0.005390083465132761, -0.011852746523309575, -0.003156323310881499, 0.007963248868264130, + 0.007864313939600763, -0.001601464053307583, -0.007701448612726584, -0.003689094083671128, 0.004088942448683545, + 0.005797757906125993, 0.000363767953875138, -0.004638914091859385, -0.003361036335001390, 0.001670607202119238, + 0.003912675164153385, 0.001194053806308097, -0.002462643018753817, -0.002623102550864884, 0.000292615997120782, + 0.002355496074120870, 0.001330430232082531, -0.001038581835235810, -0.001781186751638964, -0.000351788978766190, + 0.001208303693873524, 0.001101735902638522, -0.000232619264909279, -0.001057738392194920, -0.000542730604541516, + 0.000492910288706401, 0.000794431841612731, 0.000086110163390211, -0.000859742027523152, -0.001221700854092078, +-0.000892257134326630, -0.000334808345968833, 0.000017353012666121, 0.000089439819783633, 0.000039619752517087 } ; + +// Sample 111111 Hz, pass 10000, stop 12000, ripple 0.5 dB, atten 100 dB. For SDR-IQ. Stop 0.10800. +double quiskFilt111D4Coefs[196] = { 0.000022860383861326, 0.000070265173417164, 0.000160689233204503, 0.000301317969124407, + 0.000487289901409396, 0.000694724116305488, 0.000878636067621920, 0.000977522977076171, 0.000925771316840867, + 0.000672305339588912, 0.000201226295657756, -0.000452162019544330, -0.001196628260487459, -0.001896523042546486, +-0.002399782411882304, -0.002577406131356652, -0.002364065883391685, -0.001786962447427500, -0.000971468216463463, +-0.000117441349581108, 0.000550823899522796, 0.000848990131462632, 0.000692513431543379, 0.000131676598678132, +-0.000652871765004500, -0.001397429828707858, -0.001834711252195214, -0.001782286112376423, -0.001211048928870428, +-0.000268185005007021, 0.000757996926666718, 0.001525273309644227, 0.001751649817944780, 0.001319320688138531, + 0.000330934715806419, -0.000906405873937246, -0.001970890581915849, -0.002464861139454794, -0.002158210203051176, +-0.001085620699944977, 0.000441507737096167, 0.001920949956624443, 0.002821890381744697, 0.002772521251916338, + 0.001707992629296951, -0.000078236168178539, -0.002007203892070514, -0.003396752476301502, -0.003697747220029350, +-0.002703958879036829, -0.000657405952388967, 0.001798494644242414, 0.003814960168125246, 0.004625841420163100, + 0.003831836297877268, 0.001577797389627854, -0.001450651164746532, -0.004226329291915980, -0.005721230469088120, +-0.005278368569659456, -0.002883121089582856, 0.000773112126565357, 0.004478776308038774, 0.006893091575659842, + 0.007019574634915599, 0.004598796615908167, 0.000271268071355489, -0.004569531219948477, -0.008209861014252150, +-0.009205194241390222, -0.006929272243366950, -0.001881659896440290, 0.004379813721900100, 0.009694113972501929, + 0.012015064656800249, 0.010162418707817443, 0.004341783259066366, -0.003775178431738174, -0.011484866194632987, +-0.015901979746403339, -0.014972424881419052, -0.008312775352876528, 0.002435156726078577, 0.013913027108919607, + 0.021990371341685223, 0.023101099895195237, 0.015573244973319158, 0.000514891807100321, -0.018080409101544084, +-0.034128539734629829, -0.040997556950638683, -0.033400949657662792, -0.009105399838522790, 0.030066051833635116, + 0.078279007079479407, 0.126836610192946620, 0.166170707028785000, 0.188161385669205480, 0.188161385669205480, + 0.166170707028785000, 0.126836610192946620, 0.078279007079479407, 0.030066051833635116, -0.009105399838522790, +-0.033400949657662792, -0.040997556950638683, -0.034128539734629829, -0.018080409101544084, 0.000514891807100321, + 0.015573244973319158, 0.023101099895195237, 0.021990371341685223, 0.013913027108919607, 0.002435156726078577, +-0.008312775352876528, -0.014972424881419052, -0.015901979746403339, -0.011484866194632987, -0.003775178431738174, + 0.004341783259066366, 0.010162418707817443, 0.012015064656800249, 0.009694113972501929, 0.004379813721900100, +-0.001881659896440290, -0.006929272243366950, -0.009205194241390222, -0.008209861014252150, -0.004569531219948477, + 0.000271268071355489, 0.004598796615908167, 0.007019574634915599, 0.006893091575659842, 0.004478776308038774, + 0.000773112126565357, -0.002883121089582856, -0.005278368569659456, -0.005721230469088120, -0.004226329291915980, +-0.001450651164746532, 0.001577797389627854, 0.003831836297877268, 0.004625841420163100, 0.003814960168125246, + 0.001798494644242414, -0.000657405952388967, -0.002703958879036829, -0.003697747220029350, -0.003396752476301502, +-0.002007203892070514, -0.000078236168178539, 0.001707992629296951, 0.002772521251916338, 0.002821890381744697, + 0.001920949956624443, 0.000441507737096167, -0.001085620699944977, -0.002158210203051176, -0.002464861139454794, +-0.001970890581915849, -0.000906405873937246, 0.000330934715806419, 0.001319320688138531, 0.001751649817944780, + 0.001525273309644227, 0.000757996926666718, -0.000268185005007021, -0.001211048928870428, -0.001782286112376423, +-0.001834711252195214, -0.001397429828707858, -0.000652871765004500, 0.000131676598678132, 0.000692513431543379, + 0.000848990131462632, 0.000550823899522796, -0.000117441349581108, -0.000971468216463463, -0.001786962447427500, +-0.002364065883391685, -0.002577406131356652, -0.002399782411882304, -0.001896523042546486, -0.001196628260487459, +-0.000452162019544330, 0.000201226295657756, 0.000672305339588912, 0.000925771316840867, 0.000977522977076171, + 0.000878636067621920, 0.000694724116305488, 0.000487289901409396, 0.000301317969124407, 0.000160689233204503, + 0.000070265173417164, 0.000022860383861326} ; + +// Sample 53333 Hz, pass 20000, stop 24000, ripple 0.1 dB, atten 100 dB. For SDR-IQ. Stop 0.45000. +double quiskFilt53D1Coefs[55] = { 0.000318858391362187, 0.001662116369387873, -0.000065237353639775, -0.000854885424783294, + 0.001693126335254083, -0.002034310867663472, 0.001354648789538254, 0.000509693525155950, -0.003079059747940221, 0.005242349925004462, +-0.005626507739484119, 0.003235398612833476, 0.001883583294529978, -0.008273775245721798, 0.013282431836060808, -0.013924391581141530, + 0.008176677795586052, 0.003763659697675038, -0.018814370186261472, 0.031357304759835808, -0.034684131608746831, 0.023184334016039702, + 0.005435239957731122, -0.048724194253317150, 0.099481724552805198, -0.147334801628740130, 0.181502639813268140, 0.806108236192275450, + 0.181502639813268140, -0.147334801628740130, 0.099481724552805198, -0.048724194253317150, 0.005435239957731122, 0.023184334016039702, +-0.034684131608746831, 0.031357304759835808, -0.018814370186261472, 0.003763659697675038, 0.008176677795586052, -0.013924391581141530, + 0.013282431836060808, -0.008273775245721798, 0.001883583294529978, 0.003235398612833476, -0.005626507739484119, 0.005242349925004462, +-0.003079059747940221, 0.000509693525155950, 0.001354648789538254, -0.002034310867663472, 0.001693126335254083, -0.000854885424783294, +-0.000065237353639775, 0.001662116369387873, 0.000318858391362187 } ; + +// Sample 53333 Hz, pass 10000, stop 12000, ripple 0.5 dB, atten 100 dB. For SDR-IQ. Stop 0.22500. +double quiskFilt53D2Coefs[93] = { 0.000107966318729964, 0.000421475598772498, 0.000731343669484184, 0.000328611865414609, +-0.001457272471508304, -0.004143803290279123, -0.005736802939367581, -0.004303861070721504, -0.000415676176666607, + 0.002561521351129964, 0.001705074918770091, -0.001794491466202784, -0.003398011312785532, -0.000595401366072839, + 0.003389160538244070, 0.003117806598779015, -0.001738195724034938, -0.004960858372597238, -0.001539269216218705, + 0.004783351138955084, 0.005278243767957266, -0.001906381347753796, -0.007653356303201065, -0.003190874189611138, + 0.006849740993527085, 0.008664448318071482, -0.002035800124581497, -0.011780892066463697, -0.005873932705860416, + 0.009972743922291657, 0.014072286848767549, -0.002113998573380457, -0.018521807368841001, -0.010490752431554192, + 0.015285369742599874, 0.023814301314901452, -0.002148815334259815, -0.031770485009434718, -0.020238958138733353, + 0.027248391304817831, 0.048071554306549469, -0.002165663523025174, -0.075199791717139086, -0.060076551864238638, + 0.094944856808714839, 0.301769455144326630, 0.397829555404577600, 0.301769455144326630, 0.094944856808714839, +-0.060076551864238638, -0.075199791717139086, -0.002165663523025174, 0.048071554306549469, 0.027248391304817831, +-0.020238958138733353, -0.031770485009434718, -0.002148815334259815, 0.023814301314901452, 0.015285369742599874, +-0.010490752431554192, -0.018521807368841001, -0.002113998573380457, 0.014072286848767549, 0.009972743922291657, +-0.005873932705860416, -0.011780892066463697, -0.002035800124581497, 0.008664448318071482, 0.006849740993527085, +-0.003190874189611138, -0.007653356303201065, -0.001906381347753796, 0.005278243767957266, 0.004783351138955084, +-0.001539269216218705, -0.004960858372597238, -0.001738195724034938, 0.003117806598779015, 0.003389160538244070, +-0.000595401366072839, -0.003398011312785532, -0.001794491466202784, 0.001705074918770091, 0.002561521351129964, +-0.000415676176666607, -0.004303861070721504, -0.005736802939367581, -0.004143803290279123, -0.001457272471508304, + 0.000328611865414609, 0.000731343669484184, 0.000421475598772498, 0.000107966318729964} ; + + +// Sample 240 kHz, pass 15, stop 23.9, ripple 0. 1dB, atten 100 dB, taps 114. For 240 to 48 decimation by 5. Stop 0.09958. +double quiskFilt240D5Coefs[115] = { -0.000016728727123248, -0.000044087357718719, -0.000091924762762279, -0.000159511110578360, +-0.000239892740115764, -0.000317037848665795, -0.000366082222804719, -0.000356867696341249, -0.000260799073810572, +-0.000060362275313048, 0.000240882695147526, 0.000610784476390585, 0.000986747708265456, 0.001282232501802643, + 0.001402382039117427, 0.001267163059859552, 0.000837554837630180, 0.000138176060202843, -0.000731120724040292, +-0.001601062409512967, -0.002259171778682862, -0.002494407062419913, -0.002152575146141896, -0.001190005644388707, + 0.000289463534037753, 0.002027990436998251, 0.003646223170245011, 0.004714602533497222, 0.004852837355866500, + 0.003837221343174296, 0.001689352142256304, -0.001280461340842228, -0.004495043698374529, -0.007204192122448914, +-0.008639958370167617, -0.008205272191961563, -0.005654509302706373, -0.001219595475489302, 0.004358910000877759, + 0.009919482807219961, 0.014084647220259008, 0.015553522670126217, 0.013426967081734104, 0.007494973982180793, +-0.001584790943077927, -0.012272742073297286, -0.022322599151574144, -0.029132041059785355, -0.030214716745057827, +-0.023707788648535934, -0.008813567526928788, 0.013920929398671823, 0.042556246756071010, 0.073971248970453499, + 0.104307827587028490, 0.129576402798269570, 0.146313667429180170, 0.152169870450184460, 0.146313667429180170, + 0.129576402798269570, 0.104307827587028490, 0.073971248970453499, 0.042556246756071010, 0.013920929398671823, +-0.008813567526928788, -0.023707788648535934, -0.030214716745057827, -0.029132041059785355, -0.022322599151574144, +-0.012272742073297286, -0.001584790943077927, 0.007494973982180793, 0.013426967081734104, 0.015553522670126217, + 0.014084647220259008, 0.009919482807219961, 0.004358910000877759, -0.001219595475489302, -0.005654509302706373, +-0.008205272191961563, -0.008639958370167617, -0.007204192122448914, -0.004495043698374529, -0.001280461340842228, + 0.001689352142256304, 0.003837221343174296, 0.004852837355866500, 0.004714602533497222, 0.003646223170245011, + 0.002027990436998251, 0.000289463534037753, -0.001190005644388707, -0.002152575146141896, -0.002494407062419913, +-0.002259171778682862, -0.001601062409512967, -0.000731120724040292, 0.000138176060202843, 0.000837554837630180, + 0.001267163059859552, 0.001402382039117427, 0.001282232501802643, 0.000986747708265456, 0.000610784476390585, + 0.000240882695147526, -0.000060362275313048, -0.000260799073810572, -0.000356867696341249, -0.000366082222804719, +-0.000317037848665795, -0.000239892740115764, -0.000159511110578360, -0.000091924762762279, -0.000044087357718719, +-0.000016728727123248} ; + +// Sample 240 kHz, pass 20, stop 24, ripple 0. 1dB, atten 100 dB. For 240 to 48 decimation by 5. Pass 0.083333. Stop 0.1000. +double quiskFilt240D5CoefsSharp[245] = {0.000010509517951006, 0.000013600436904199, 0.000014114646487651, + 0.000003107336183074, -0.000027062229851180, -0.000082599263842674, -0.000165408687939920, -0.000270459677107609, +-0.000384429386564009, -0.000486513762577162, -0.000551912068282870, -0.000557575961050952, -0.000489097193751572, +-0.000346866350142718, -0.000149400346186253, 0.000067944169519721, 0.000259321418469537, 0.000379109945031964, + 0.000394500403136929, 0.000296443107959613, 0.000105514360701591, -0.000130126194004534, -0.000345130836201596, +-0.000473961146691502, -0.000470132844463270, -0.000322205344194264, -0.000061120176458703, 0.000244508351024915, + 0.000505224922624527, 0.000635967434817550, 0.000582869672082906, 0.000343410640475203, -0.000027211329865173, +-0.000427051494167815, -0.000734325622577685, -0.000843497343068510, -0.000700190684320865, -0.000323704342054218, + 0.000191371674406576, 0.000697200136082736, 0.001033942501059566, 0.001079068090563369, 0.000789896649978617, + 0.000225095253699184, -0.000463869809966486, -0.001071638023123453, -0.001398840064879135, -0.001315934285574502, +-0.000810990907316290, -0.000004610004754826, 0.000875943963655002, 0.001559026923347157, 0.001810835543451637, + 0.001513186414012800, 0.000711563768288702, -0.000384610343377712, -0.001453941368175342, -0.002155359985238479, +-0.002234880299660999, -0.001614005062810786, -0.000429922430657965, 0.000989560561609744, 0.002214005025736412, + 0.002838581123665033, 0.002615068817313840, 0.001544038090357682, -0.000104494434130949, -0.001854539656237390, +-0.003158534255089867, -0.003565698051769338, -0.002873486424864839, -0.001212438364790582, 0.000968078677012544, + 0.003016832609770853, 0.004271507641806298, 0.004268577543475138, 0.002906991659042924, 0.000509839095017221, +-0.002243915738203439, -0.004506439605309381, -0.005516963680092404, -0.004850900074620331, -0.002582471594385039, + 0.000697817320287101, 0.004028242154893376, 0.006350748353554076, 0.006839211177606702, 0.005181509596969096, + 0.001722314463339247, -0.002590160933654113, -0.006453095775103189, -0.008591647653124229, -0.008165982840134313, +-0.005076656227616793, -0.000063471506879233, 0.005451413373916889, 0.009749704157369357, 0.011332344600275311, + 0.009414890577675568, 0.004246680386224628, -0.002862043391342491, -0.009841507474776875, -0.014431368776294761, +-0.014868975905553507, -0.010498438296855372, -0.002106968919934075, 0.008141920985575500, 0.017201630945497853, + 0.021962093122837603, 0.020191237691200150, 0.011339390591466089, -0.003037076058820319, -0.019391273155317083, +-0.032862864929577094, -0.038387061811562206, -0.031979063119985378, -0.011872162453893439, 0.020795139196288157, + 0.061948624205024894, 0.105252254728618600, 0.143357336865708950, 0.169447992824642290, 0.178721289066174270, + 0.169447992824642290, 0.143357336865708950, 0.105252254728618600, 0.061948624205024894, 0.020795139196288157, +-0.011872162453893439, -0.031979063119985378, -0.038387061811562206, -0.032862864929577094, -0.019391273155317083, +-0.003037076058820319, 0.011339390591466089, 0.020191237691200150, 0.021962093122837603, 0.017201630945497853, + 0.008141920985575500, -0.002106968919934075, -0.010498438296855372, -0.014868975905553507, -0.014431368776294761, +-0.009841507474776875, -0.002862043391342491, 0.004246680386224628, 0.009414890577675568, 0.011332344600275311, + 0.009749704157369357, 0.005451413373916889, -0.000063471506879233, -0.005076656227616793, -0.008165982840134313, +-0.008591647653124229, -0.006453095775103189, -0.002590160933654113, 0.001722314463339247, 0.005181509596969096, + 0.006839211177606702, 0.006350748353554076, 0.004028242154893376, 0.000697817320287101, -0.002582471594385039, +-0.004850900074620331, -0.005516963680092404, -0.004506439605309381, -0.002243915738203439, 0.000509839095017221, + 0.002906991659042924, 0.004268577543475138, 0.004271507641806298, 0.003016832609770853, 0.000968078677012544, +-0.001212438364790582, -0.002873486424864839, -0.003565698051769338, -0.003158534255089867, -0.001854539656237390, +-0.000104494434130949, 0.001544038090357682, 0.002615068817313840, 0.002838581123665033, 0.002214005025736412, + 0.000989560561609744, -0.000429922430657965, -0.001614005062810786, -0.002234880299660999, -0.002155359985238479, +-0.001453941368175342, -0.000384610343377712, 0.000711563768288702, 0.001513186414012800, 0.001810835543451637, + 0.001559026923347157, 0.000875943963655002, -0.000004610004754826, -0.000810990907316290, -0.001315934285574502, +-0.001398840064879135, -0.001071638023123453, -0.000463869809966486, 0.000225095253699184, 0.000789896649978617, + 0.001079068090563369, 0.001033942501059566, 0.000697200136082736, 0.000191371674406576, -0.000323704342054218, +-0.000700190684320865, -0.000843497343068510, -0.000734325622577685, -0.000427051494167815, -0.000027211329865173, + 0.000343410640475203, 0.000582869672082906, 0.000635967434817550, 0.000505224922624527, 0.000244508351024915, +-0.000061120176458703, -0.000322205344194264, -0.000470132844463270, -0.000473961146691502, -0.000345130836201596, +-0.000130126194004534, 0.000105514360701591, 0.000296443107959613, 0.000394500403136929, 0.000379109945031964, + 0.000259321418469537, 0.000067944169519721, -0.000149400346186253, -0.000346866350142718, -0.000489097193751572, +-0.000557575961050952, -0.000551912068282870, -0.000486513762577162, -0.000384429386564009, -0.000270459677107609, +-0.000165408687939920, -0.000082599263842674, -0.000027062229851180, 0.000003107336183074, 0.000014114646487651, + 0.000013600436904199, 0.000010509517951006} ; + +// Sample 48 kHz, pass 10, stop 12, ripple 0.1 dB, atten 100 dB. Pass 0.20833. Stop 0.25000. +double quiskFilt48dec24Coefs[98] = { 0.000036864882767612, 0.000009858596392836, -0.000330770380800406, -0.001009174072411182, +-0.001404963853116591, -0.000770236458542885, 0.000523955998497470, 0.000947009978368078, -0.000170647420729277, -0.001215337186275615, +-0.000339882761263432, 0.001380697691347411, 0.001052816245779061, -0.001289457483737270, -0.001905995022750999, + 0.000806360758757266, 0.002752294066736399, 0.000151371304906695, -0.003382080182092562, -0.001586160863198252, + 0.003550356871637037, 0.003397108074666923, -0.003014198882506884, -0.005368247151920206, 0.001578030229641401, + 0.007172700113962798, 0.000859017219495951, -0.008394441438252027, -0.004260927420042382, 0.008565389049746911, + 0.008423570232799422, -0.007213182430375741, -0.012960608818773384, 0.003906137602881253, 0.017299646590834120, + 0.001710647317106962, -0.020687435471124054, -0.009911496013769150, 0.022192116474879110, 0.020997544995749250, +-0.020577450597980520, -0.035567363858049900, 0.013892294138831605, 0.055472458661532512, 0.002296277239682308, +-0.087929779622300100, -0.045452533452794319, 0.182125569604839360, 0.410917417297433360, 0.410917417297433360, + 0.182125569604839360, -0.045452533452794319, -0.087929779622300100, 0.002296277239682308, 0.055472458661532512, + 0.013892294138831605, -0.035567363858049900, -0.020577450597980520, 0.020997544995749250, 0.022192116474879110, +-0.009911496013769150, -0.020687435471124054, 0.001710647317106962, 0.017299646590834120, 0.003906137602881253, +-0.012960608818773384, -0.007213182430375741, 0.008423570232799422, 0.008565389049746911, -0.004260927420042382, +-0.008394441438252027, 0.000859017219495951, 0.007172700113962798, 0.001578030229641401, -0.005368247151920206, +-0.003014198882506884, 0.003397108074666923, 0.003550356871637037, -0.001586160863198252, -0.003382080182092562, + 0.000151371304906695, 0.002752294066736399, 0.000806360758757266, -0.001905995022750999, -0.001289457483737270, + 0.001052816245779061, 0.001380697691347411, -0.000339882761263432, -0.001215337186275615, -0.000170647420729277, + 0.000947009978368078, 0.000523955998497470, -0.000770236458542885, -0.001404963853116591, -0.001009174072411182, +-0.000330770380800406, 0.000009858596392836, 0.000036864882767612} ; + +// Sample 16 kHz, pass 3, stop 4, ripple 0.2 dB, atten 100 dB. Stop 0.25000. +double quiskFilt16dec8Coefs[62] = { -0.000124177474244557, -0.000430523605276357, -0.000493631990211104, 0.000494123472239155, + 0.002450538023220896, 0.003434046886116560, 0.001430964941031747, -0.002160470594532627, -0.002743908756760310, + 0.001447692179523467, 0.005035286516949985, 0.001559830515735925, -0.005832171392166522, -0.005961983009450190, + 0.004090355806650406, 0.010727470109490524, 0.001295745938037953, -0.013610977881243281, -0.010206097747183476, + 0.011968191835149432, 0.021092443344418406, -0.003254546341513048, -0.030789280172024487, -0.014459667926688211, + 0.034543627845998542, 0.042879451356168191, -0.025057567311424923, -0.087496290237351254, -0.017166635170977933, + 0.194607349925601910, 0.387989214201396540, 0.387989214201396540, 0.194607349925601910, -0.017166635170977933, +-0.087496290237351254, -0.025057567311424923, 0.042879451356168191, 0.034543627845998542, -0.014459667926688211, +-0.030789280172024487, -0.003254546341513048, 0.021092443344418406, 0.011968191835149432, -0.010206097747183476, +-0.013610977881243281, 0.001295745938037953, 0.010727470109490524, 0.004090355806650406, -0.005961983009450190, +-0.005832171392166522, 0.001559830515735925, 0.005035286516949985, 0.001447692179523467, -0.002743908756760310, +-0.002160470594532627, 0.001430964941031747, 0.003434046886116560, 0.002450538023220896, 0.000494123472239155, +-0.000493631990211104, -0.000430523605276357, -0.000124177474244557} ; + + +// Sample 24 kHz, pass 4, stop 6, ripple 0.1 dB, atten 100 dB. Stop 0.25000. +double quiskAudio24p4Coefs[50] = { 0.000157528548309112, 0.000724067233656635, 0.001494063979696902, 0.001415398667382404, +-0.000379600362177000, -0.002568955219195720, -0.001955131144563022, 0.002290648015950155, 0.005318866809894723, + 0.001269605748864858, -0.007123779005412860, -0.008160775426007658, 0.003744714029953210, 0.015299947817479908, + 0.007159589230094381, -0.016608567214549036, -0.024058672575662729, 0.004961464480378132, 0.040021693556365548, + 0.025370838603802327, -0.042357473426880975, -0.080147218603820458, 0.005640775946851126, 0.201560113392096980, + 0.369146036302117400, 0.369146036302117400, 0.201560113392096980, 0.005640775946851126, -0.080147218603820458, +-0.042357473426880975, 0.025370838603802327, 0.040021693556365548, 0.004961464480378132, -0.024058672575662729, +-0.016608567214549036, 0.007159589230094381, 0.015299947817479908, 0.003744714029953210, -0.008160775426007658, +-0.007123779005412860, 0.001269605748864858, 0.005318866809894723, 0.002290648015950155, -0.001955131144563022, +-0.002568955219195720, -0.000379600362177000, 0.001415398667382404, 0.001494063979696902, 0.000724067233656635, + 0.000157528548309112} ; + +// Sample 24 kHz, pass 6, stop 8, ripple 0.5 dB, atten 80 dB. Stop 0.33333. +double quiskAudio24p6Coefs[36] = { 0.001199140008010727, 0.005953815908571521, 0.008621055763448699, -0.000319602525571569, +-0.008665733839154772, 0.003122601697996235, 0.011839435384729619, -0.008973143363910490, -0.014884256392667099, 0.019451226278492342, + 0.015232717446420085, -0.036364345084318225, -0.008723687718470995, 0.063232433222709966, -0.014104739941611717, -0.115885442639735490, + 0.103975832351096060, 0.487646324142562260, 0.487646324142562260, 0.103975832351096060, -0.115885442639735490, -0.014104739941611717, + 0.063232433222709966, -0.008723687718470995, -0.036364345084318225, 0.015232717446420085, 0.019451226278492342, -0.014884256392667099, +-0.008973143363910490, 0.011839435384729619, 0.003122601697996235, -0.008665733839154772, -0.000319602525571569, 0.008621055763448699, + 0.005953815908571521, 0.001199140008010727} ; + +// Sample 48 kHz, pass 6, stop 8, ripple 0.5 dB, atten 80 dB. Stop 0.16667. +double quiskAudio48p6Coefs[71] = { 0.000324677779792651, 0.001038192405843851, 0.002082337464742640, 0.002938055116822665, + 0.002811674671240099, 0.001132278163758895, -0.001835394694865431, -0.004766409947196539, -0.005833007003128954, -0.003914015660218843, + 0.000296099005521212, 0.004239593489554206, 0.004918390640978499, 0.001100872619673516, -0.005241634309386784, +-0.009658192522859132, -0.008117324326241930, -0.000324815284776402, 0.009102631659469559, 0.013098226740520415, + 0.007176675441219364, -0.006349840429806648, -0.018522542400385748, -0.019189364957006558, -0.004749023128788906, + 0.017303403623348305, 0.031306220127229140, 0.023400239035184393, -0.007229519056042042, -0.043717246110810550, +-0.058176618846235130, -0.027325746983863020, 0.051711450483087174, 0.155566868220561340, 0.243667260926508620, + 0.278152523812557280, 0.243667260926508620, 0.155566868220561340, 0.051711450483087174, -0.027325746983863020, +-0.058176618846235130, -0.043717246110810550, -0.007229519056042042, 0.023400239035184393, 0.031306220127229140, + 0.017303403623348305, -0.004749023128788906, -0.019189364957006558, -0.018522542400385748, -0.006349840429806648, + 0.007176675441219364, 0.013098226740520415, 0.009102631659469559, -0.000324815284776402, -0.008117324326241930, +-0.009658192522859132, -0.005241634309386784, 0.001100872619673516, 0.004918390640978499, 0.004239593489554206, + 0.000296099005521212, -0.003914015660218843, -0.005833007003128954, -0.004766409947196539, -0.001835394694865431, + 0.001132278163758895, 0.002811674671240099, 0.002938055116822665, 0.002082337464742640, 0.001038192405843851, + 0.000324677779792651} ; + +// Sample 96 kHz, pass 6, stop 40, ripple 0.1 dB, atten 100 dB. Stop 0.41667. +double quiskAudio96Coefs[11] = { -0.004756607210954994, -0.022893760473075838, -0.023609616122334920, + 0.078161168368381481, 0.277880235815005590, 0.388489826271597950, 0.277880235815005590, + 0.078161168368381481, -0.023609616122334920, -0.022893760473075838, -0.004756607210954994} ; + + +// Sample 12000 kHz, high pass, stop 180, pass 300, ripple 0.2 dB, atten 80 dB. +double quiskAudioFmHpCoefs[309] = { 0.004847574947800705, -0.001211970386842814, -0.001072277374239886, -0.000949749961336003, +-0.000841696354699768, -0.000745515693376396, -0.000658731927598885, -0.000579117971409598, -0.000504808183463150, -0.000434052387522716, +-0.000365311474947399, -0.000297420057921267, -0.000229505695217796, -0.000160920338573639, -0.000091306103977099, -0.000020600671979957, + 0.000051022545303487, 0.000123144667419350, 0.000195091536211946, 0.000265999359621072, 0.000334844261309097, 0.000400429077199748, + 0.000461460673600523, 0.000516639603401830, 0.000564613893906762, 0.000603966827912324, 0.000633383885155741, 0.000651723486505544, + 0.000657887414710607, 0.000650908078289207, 0.000630221666830102, 0.000595435888069700, 0.000546108904674455, 0.000484276489349049, + 0.000401543327440382, 0.000321419603506285, 0.000213106256109367, 0.000098353002391692, -0.000019740287652141, -0.000144911454925575, +-0.000277381849908065, -0.000413476038173425, -0.000548427361576298, -0.000678490099554352, -0.000801069365710719, -0.000914049078670187, +-0.001015286248346293, -0.001102331400542105, -0.001172396297916355, -0.001222632870457348, -0.001250498303855702, -0.001253983783014675, +-0.001231724573461644, -0.001182999511712966, -0.001107609474592948, -0.001005802502109255, -0.000878333102915519, -0.000726459269042778, +-0.000551839538058223, -0.000356598632546370, -0.000143511677076383, 0.000084082690787220, 0.000322510489680255, 0.000567640643123008, + 0.000814725473802231, 0.001058851063175263, 0.001295215851931261, 0.001517809497406495, 0.001723717744529424, 0.001902986849115314, + 0.002058196188280081, 0.002178885915107971, 0.002262004073121292, 0.002306568447922007, 0.002308817130050681, 0.002265197627398382, + 0.002174415827424258, 0.002036784086082496, 0.001852852576328433, 0.001623326794876901, 0.001349852467618926, 0.001035442578833791, + 0.000684336173595900, 0.000301699601011732, -0.000106613479977550, -0.000534173868829619, -0.000973944167066713, -0.001418238552947833, +-0.001858773604862586, -0.002286765126902860, -0.002693131197271969, -0.003068872591348129, -0.003405360512862820, -0.003694294048148267, +-0.003927679645897309, -0.004098170195163846, -0.004199334823619039, -0.004225516845056660, -0.004171926884864147, -0.004035318109791245, +-0.003814086552001789, -0.003507185822613928, -0.003116677958603595, -0.002643536244472083, -0.002094553515010935, -0.001473198308268938, +-0.000787663079055384, -0.000048057914319065, 0.000736224014748010, 0.001553680380245313, 0.002390615203692300, 0.003232587312827377, + 0.004064917562862491, 0.004872099692091644, 0.005637649839955575, 0.006344792681250736, 0.006977074214765010, 0.007518456060848391, + 0.007953298539805269, 0.008266566568951741, 0.008444193362869966, 0.008473441636160825, 0.008343141792745356, 0.008043815615010122, + 0.007567858472781280, 0.006909789777373219, 0.006066331643824790, 0.005036377728498742, 0.003821244456796700, 0.002425048400384444, + 0.000854549919626146, -0.000881361873217043, -0.002771584559469569, -0.004802304296173217, -0.006957451254254373, -0.009219821548686471, +-0.011569065498728633, -0.013984260863667768, -0.016442032516463558, -0.018919305853375375, -0.021391356610149178, -0.023832595189340611, +-0.026218718241990988, -0.028524575634244292, -0.030725193547532524, -0.032797022202043544, -0.034717887030001365, -0.036466683292335302, +-0.038023870249327039, -0.039372161079335152, -0.040496648982101487, -0.041384719119703033, -0.042026270620733261, -0.042414057053836371, + 0.957456210208300410, -0.042414057053836371, -0.042026270620733261, -0.041384719119703033, -0.040496648982101487, -0.039372161079335152, +-0.038023870249327039, -0.036466683292335302, -0.034717887030001365, -0.032797022202043544, -0.030725193547532524, -0.028524575634244292, +-0.026218718241990988, -0.023832595189340611, -0.021391356610149178, -0.018919305853375375, -0.016442032516463558, -0.013984260863667768, +-0.011569065498728633, -0.009219821548686471, -0.006957451254254373, -0.004802304296173217, -0.002771584559469569, -0.000881361873217043, + 0.000854549919626146, 0.002425048400384444, 0.003821244456796700, 0.005036377728498742, 0.006066331643824790, 0.006909789777373219, + 0.007567858472781280, 0.008043815615010122, 0.008343141792745356, 0.008473441636160825, 0.008444193362869966, 0.008266566568951741, + 0.007953298539805269, 0.007518456060848391, 0.006977074214765010, 0.006344792681250736, 0.005637649839955575, 0.004872099692091644, + 0.004064917562862491, 0.003232587312827377, 0.002390615203692300, 0.001553680380245313, 0.000736224014748010, -0.000048057914319065, +-0.000787663079055384, -0.001473198308268938, -0.002094553515010935, -0.002643536244472083, -0.003116677958603595, -0.003507185822613928, +-0.003814086552001789, -0.004035318109791245, -0.004171926884864147, -0.004225516845056660, -0.004199334823619039, -0.004098170195163846, +-0.003927679645897309, -0.003694294048148267, -0.003405360512862820, -0.003068872591348129, -0.002693131197271969, -0.002286765126902860, +-0.001858773604862586, -0.001418238552947833, -0.000973944167066713, -0.000534173868829619, -0.000106613479977550, 0.000301699601011732, + 0.000684336173595900, 0.001035442578833791, 0.001349852467618926, 0.001623326794876901, 0.001852852576328433, 0.002036784086082496, + 0.002174415827424258, 0.002265197627398382, 0.002308817130050681, 0.002306568447922007, 0.002262004073121292, 0.002178885915107971, + 0.002058196188280081, 0.001902986849115314, 0.001723717744529424, 0.001517809497406495, 0.001295215851931261, 0.001058851063175263, + 0.000814725473802231, 0.000567640643123008, 0.000322510489680255, 0.000084082690787220, -0.000143511677076383, -0.000356598632546370, +-0.000551839538058223, -0.000726459269042778, -0.000878333102915519, -0.001005802502109255, -0.001107609474592948, -0.001182999511712966, +-0.001231724573461644, -0.001253983783014675, -0.001250498303855702, -0.001222632870457348, -0.001172396297916355, -0.001102331400542105, +-0.001015286248346293, -0.000914049078670187, -0.000801069365710719, -0.000678490099554352, -0.000548427361576298, -0.000413476038173425, +-0.000277381849908065, -0.000144911454925575, -0.000019740287652141, 0.000098353002391692, 0.000213106256109367, 0.000321419603506285, + 0.000401543327440382, 0.000484276489349049, 0.000546108904674455, 0.000595435888069700, 0.000630221666830102, 0.000650908078289207, + 0.000657887414710607, 0.000651723486505544, 0.000633383885155741, 0.000603966827912324, 0.000564613893906762, 0.000516639603401830, + 0.000461460673600523, 0.000400429077199748, 0.000334844261309097, 0.000265999359621072, 0.000195091536211946, 0.000123144667419350, + 0.000051022545303487, -0.000020600671979957, -0.000091306103977099, -0.000160920338573639, -0.000229505695217796, -0.000297420057921267, +-0.000365311474947399, -0.000434052387522716, -0.000504808183463150, -0.000579117971409598, -0.000658731927598885, -0.000745515693376396, +-0.000841696354699768, -0.000949749961336003, -0.001072277374239886, -0.001211970386842814, 0.004847574947800705} ; + +// Sample 24 kHz, pass 3, stop 4, ripple 0.1 dB, atten 100 dB. Stop 0.166667. +double quiskAudio24p3Coefs[100] = { 0.000040101217607380, 0.000144142945632828, 0.000313272623844654, 0.000472482653992188, + 0.000478145332060253, 0.000196098255702949, -0.000370577202968848, -0.000996638165520275, -0.001297816111521122, +-0.000964536466628102, -0.000042557313824444, 0.000962997869327471, 0.001323534221298773, 0.000594056697947640, +-0.000938434034616708, -0.002262533962755747, -0.002248428515927852, -0.000532704303549466, 0.001971754193404763, + 0.003472380925898466, 0.002523293296727912, -0.000709634306372636, -0.004178686614322947, -0.005154383468615377, +-0.002273829954161664, 0.003035630406357807, 0.007068054286328254, 0.006315276617742645, 0.000339547080891264, +-0.007198952625282303, -0.010594445062611715, -0.006237154402398545, 0.004041031055408734, 0.013268415977346956, + 0.013808366838176838, 0.003317264370573068, -0.012230819216356600, -0.021457570785737779, -0.015397758077551751, + 0.004833972586296557, 0.026490252025494231, 0.032271888015886901, 0.012604612531251926, -0.024801184242399594, +-0.055520053090148611, -0.050680798756218122, 0.005272625250748675, 0.102090078311359670, 0.205131072151264500, + 0.271331067474994470, 0.271331067474994470, 0.205131072151264500, 0.102090078311359670, 0.005272625250748675, +-0.050680798756218122, -0.055520053090148611, -0.024801184242399594, 0.012604612531251926, 0.032271888015886901, + 0.026490252025494231, 0.004833972586296557, -0.015397758077551751, -0.021457570785737779, -0.012230819216356600, + 0.003317264370573068, 0.013808366838176838, 0.013268415977346956, 0.004041031055408734, -0.006237154402398545, +-0.010594445062611715, -0.007198952625282303, 0.000339547080891264, 0.006315276617742645, 0.007068054286328254, + 0.003035630406357807, -0.002273829954161664, -0.005154383468615377, -0.004178686614322947, -0.000709634306372636, + 0.002523293296727912, 0.003472380925898466, 0.001971754193404763, -0.000532704303549466, -0.002248428515927852, +-0.002262533962755747, -0.000938434034616708, 0.000594056697947640, 0.001323534221298773, 0.000962997869327471, +-0.000042557313824444, -0.000964536466628102, -0.001297816111521122, -0.000996638165520275, -0.000370577202968848, + 0.000196098255702949, 0.000478145332060253, 0.000472482653992188, 0.000313272623844654, 0.000144142945632828, + 0.000040101217607380} ; + +// Sample 166.667 kHz, pass 20.000, stop 23.900, ripple 0.1dB, atten 100 dB. Stop 0.143400. +double quiskFilt167D3Coefs[174] = { 0.000030576791453527, 0.000100024508790516, 0.000219490573577180, 0.000367267217464130, + 0.000487951714515836, 0.000506290951844816, 0.000363119613743102, 0.000058217319683104, -0.000325800746668442, +-0.000641682936184189, -0.000741147106205377, -0.000558069498091234, -0.000164677374303788, 0.000241795422764969, + 0.000430784306851279, 0.000269110113911442, -0.000183008351848305, -0.000678061334270815, -0.000905480029500586, +-0.000676768412048412, -0.000064217209958982, 0.000604041196303946, 0.000913992713709960, 0.000618300484733981, +-0.000178595029186343, -0.001034200731311528, -0.001402993970581089, -0.000974211666713609, 0.000092170056602371, + 0.001200780364176702, 0.001644807535570492, 0.001049188500933230, -0.000339340877146445, -0.001726815143299963, +-0.002213609343829470, -0.001365082350867275, 0.000444074067558948, 0.002167326762557576, 0.002679800165208473, + 0.001499801473272823, -0.000816400722026263, -0.002906147604258461, -0.003386440545728839, -0.001750807523066417, + 0.001194559262667584, 0.003696427504621467, 0.004080958623548870, 0.001851416220932293, -0.001849699778520349, +-0.004790658557602842, -0.004973805523919799, -0.001961292686306448, 0.002659261470241232, 0.006072015731008707, + 0.005920856423841668, 0.001893698122810163, -0.003838202172739613, -0.007743253601342105, -0.007066127284542626, +-0.001708831992923160, 0.005393163352848182, 0.009818853048919281, 0.008356595075109337, 0.001237190414144524, +-0.007594080720116375, -0.012578375307378011, -0.009950345325599911, -0.000416149899767642, 0.010713667739836119, + 0.016347877720485654, 0.011969545448953062, -0.001060994469278557, -0.015508256818753343, -0.022033387392024387, +-0.014893466276995419, 0.003733297630878139, 0.023713405546777432, 0.031919145545518959, 0.019968942416937793, +-0.009436963490412478, -0.041522182058827371, -0.055167482103195595, -0.033140893435705220, 0.028140747437596390, + 0.114536627613187850, 0.198460066797957520, 0.250031620638361150, 0.250031620638361150, 0.198460066797957520, + 0.114536627613187850, 0.028140747437596390, -0.033140893435705220, -0.055167482103195595, -0.041522182058827371, +-0.009436963490412478, 0.019968942416937793, 0.031919145545518959, 0.023713405546777432, 0.003733297630878139, +-0.014893466276995419, -0.022033387392024387, -0.015508256818753343, -0.001060994469278557, 0.011969545448953062, + 0.016347877720485654, 0.010713667739836119, -0.000416149899767642, -0.009950345325599911, -0.012578375307378011, +-0.007594080720116375, 0.001237190414144524, 0.008356595075109337, 0.009818853048919281, 0.005393163352848182, +-0.001708831992923160, -0.007066127284542626, -0.007743253601342105, -0.003838202172739613, 0.001893698122810163, + 0.005920856423841668, 0.006072015731008707, 0.002659261470241232, -0.001961292686306448, -0.004973805523919799, +-0.004790658557602842, -0.001849699778520349, 0.001851416220932293, 0.004080958623548870, 0.003696427504621467, + 0.001194559262667584, -0.001750807523066417, -0.003386440545728839, -0.002906147604258461, -0.000816400722026263, + 0.001499801473272823, 0.002679800165208473, 0.002167326762557576, 0.000444074067558948, -0.001365082350867275, +-0.002213609343829470, -0.001726815143299963, -0.000339340877146445, 0.001049188500933230, 0.001644807535570492, + 0.001200780364176702, 0.000092170056602371, -0.000974211666713609, -0.001402993970581089, -0.001034200731311528, +-0.000178595029186343, 0.000618300484733981, 0.000913992713709960, 0.000604041196303946, -0.000064217209958982, +-0.000676768412048412, -0.000905480029500586, -0.000678061334270815, -0.000183008351848305, 0.000269110113911442, + 0.000430784306851279, 0.000241795422764969, -0.000164677374303788, -0.000558069498091234, -0.000741147106205377, +-0.000641682936184189, -0.000325800746668442, 0.000058217319683104, 0.000363119613743102, 0.000506290951844816, + 0.000487951714515836, 0.000367267217464130, 0.000219490573577180, 0.000100024508790516, 0.000030576791453527} ; + +// Sample 8000 Hz, stop 0 to 120, pass 300 to 2700, stop 2900 to 4000, ripple 0.1dB, atten 100 dB. +double quiskFiltTx8kAudioB[168] = { 0.000382863369135811, 0.001125903700328483, 0.000925837447276999, -0.000374734941094989, +-0.000888985517068820, -0.000361216817648551, -0.000190964056763804, -0.000521640215250387, -0.000810984560171181, +-0.000586556150016652, -0.000283591051604510, -0.000870238259790123, -0.000852903625374087, 0.000024159967210229, +-0.000630232168612451, -0.000785665131620066, 0.000569610938158391, -0.000138017351197930, -0.000362220277410620, + 0.001366339327936554, 0.000470327395373754, 0.000284347316273074, 0.002255344271539108, 0.000942593074105167, + 0.000895615209137272, 0.002941734992852122, 0.000961093640197151, 0.001177319952191160, 0.003099108113624071, + 0.000249508373597955, 0.000915783375976514, 0.002501346935887695, -0.001290867165414506, 0.000106788350951055, + 0.001148919726851922, -0.003476112709865105, -0.000973631179709113, -0.000674076242033701, -0.005811280332409654, +-0.001784274911171613, -0.002440004582391299, -0.007574097052077259, -0.001651967564004321, -0.003534348839131837, +-0.008009304387660080, 0.000005086297602882, -0.003482125314769407, -0.006580016297377269, 0.003414827979152467, +-0.002192304121179012, -0.003207666910599826, 0.008239448308648542, -0.000120276288831242, 0.001601392930489681, + 0.013506207426436229, 0.001723883024608883, 0.006793369654665180, 0.017715945730523756, 0.001956966866997749, + 0.011004187010827985, 0.019134477971688185, -0.000836412988146811, 0.012945050783659821, 0.016195220864669575, +-0.007677125676925340, 0.011863897915831447, 0.007909156115753185, -0.018792331215007903, 0.007957114843116177, +-0.005919014059116164, -0.033405211299572850, 0.002675271476407882, -0.024817610540172161, -0.049749838513716206, +-0.000915002981442826, -0.048502552862820653, -0.065383587718200784, 0.003175445066804758, -0.080194918286764838, +-0.077682133405468937, 0.033727063680071466, -0.150760186145271510, -0.084457338944868982, 0.505660603990009560, + 0.505660603990009560, -0.084457338944868982, -0.150760186145271510, 0.033727063680071466, -0.077682133405468937, +-0.080194918286764838, 0.003175445066804758, -0.065383587718200784, -0.048502552862820653, -0.000915002981442826, +-0.049749838513716206, -0.024817610540172161, 0.002675271476407882, -0.033405211299572850, -0.005919014059116164, + 0.007957114843116177, -0.018792331215007903, 0.007909156115753185, 0.011863897915831447, -0.007677125676925340, + 0.016195220864669575, 0.012945050783659821, -0.000836412988146811, 0.019134477971688185, 0.011004187010827985, + 0.001956966866997749, 0.017715945730523756, 0.006793369654665180, 0.001723883024608883, 0.013506207426436229, + 0.001601392930489681, -0.000120276288831242, 0.008239448308648542, -0.003207666910599826, -0.002192304121179012, + 0.003414827979152467, -0.006580016297377269, -0.003482125314769407, 0.000005086297602882, -0.008009304387660080, +-0.003534348839131837, -0.001651967564004321, -0.007574097052077259, -0.002440004582391299, -0.001784274911171613, +-0.005811280332409654, -0.000674076242033701, -0.000973631179709113, -0.003476112709865105, 0.001148919726851922, + 0.000106788350951055, -0.001290867165414506, 0.002501346935887695, 0.000915783375976514, 0.000249508373597955, + 0.003099108113624071, 0.001177319952191160, 0.000961093640197151, 0.002941734992852122, 0.000895615209137272, + 0.000942593074105167, 0.002255344271539108, 0.000284347316273074, 0.000470327395373754, 0.001366339327936554, +-0.000362220277410620, -0.000138017351197930, 0.000569610938158391, -0.000785665131620066, -0.000630232168612451, + 0.000024159967210229, -0.000852903625374087, -0.000870238259790123, -0.000283591051604510, -0.000586556150016652, +-0.000810984560171181, -0.000521640215250387, -0.000190964056763804, -0.000361216817648551, -0.000888985517068820, +-0.000374734941094989, 0.000925837447276999, 0.001125903700328483, 0.000382863369135811 } ; +// Sample 48000 Hz, pass 2350, stop 2650, ripple 1 dB, atten 80 dB. Stop 0.05521. +double quiskMic5Filt48Coefs[424] = { +-0.000087601196745517, -0.000119178480453066, -0.000192494530734137, -0.000287398500897765, -0.000404455107555149, +-0.000542187350923721, -0.000697354821092750, -0.000864361814542882, -0.001035675409027330, -0.001201670675683178, +-0.001351260760102592, -0.001472389090721280, -0.001552963425946309, -0.001581622501567850, -0.001548788902291486, +-0.001447577149496593, -0.001274757191818737, -0.001031365182705646, -0.000723138798454071, -0.000360540808884225, + 0.000041472210381792, 0.000464151153253057, 0.000885978417045319, 0.001284080850383070, 0.001635786964228304, + 0.001920322587244723, 0.002120431736364362, 0.002223894227313585, 0.002224702189457267, 0.002123834204231122, + 0.001929455560771270, 0.001656578667677397, 0.001326124950893504, 0.000963492246325779, 0.000596656779876184, + 0.000254003709475283, -0.000037984405595945, -0.000256935860609037, -0.000386544706976665, -0.000418046328997344, +-0.000351100298847373, -0.000193939445451714, 0.000037212952694738, 0.000319460819342812, 0.000625164037239141, + 0.000924363583767082, 0.001187470307244314, 0.001388000318914036, 0.001505069987035192, 0.001525437805536890, + 0.001444870661224978, 0.001268714805496182, 0.001011568778937986, 0.000696080786323511, 0.000350937369521106, + 0.000008234786560396, -0.000299551487706380, -0.000542701008001189, -0.000697204883792079, -0.000747233312787443, +-0.000686876298204384, -0.000520952456912102, -0.000264798296929030, 0.000056986807722520, 0.000412734842368625, + 0.000766669398901066, 0.001082346669587512, 0.001326263088537886, 0.001471265194417923, 0.001499431895309172, + 0.001404113830652116, 0.001190910569750263, 0.000877443544384873, 0.000491924577078300, 0.000070618732748754, +-0.000345567500376679, -0.000715070316355716, -0.000999834598355945, -0.001169257920406941, -0.001203497801923855, +-0.001095797960360223, -0.000853559853825754, -0.000498020971159122, -0.000062520041312805, 0.000410511052497376, + 0.000873571510876301, 0.001278748950006680, 0.001582567444665569, 0.001750535054942567, 0.001760927137558775, + 0.001607388781175447, 0.001300061195708352, 0.000865065069005933, 0.000342348399080757, -0.000217942303300231, +-0.000760241586711499, -0.001229053244090960, -0.001574604672635160, -0.001758102209059370, -0.001756034066558804, +-0.001563058384174837, -0.001193131136885269, -0.000678706705799908, -0.000068024295664869, 0.000579300435978082, + 0.001197989750585395, 0.001723580332124720, 0.002099032072900320, 0.002280794678156941, 0.002243699390601673, + 0.001984133400353534, 0.001521121064322945, 0.000895127993320633, 0.000164637922390302, -0.000599227139535745, +-0.001319555291352399, -0.001921314886803284, -0.002339087231340391, -0.002524079501171732, -0.002449675079358536, +-0.002114908771525688, -0.001545437314172064, -0.000791823925944463, 0.000074782605168353, 0.000969218460672265, + 0.001800534718138737, 0.002481100933662590, 0.002935686794045762, 0.003109591237339856, 0.002974959532860613, + 0.002534577733743369, 0.001822671724082916, 0.000902520745944448, -0.000138980947793396, -0.001199369993895262, +-0.002170536435942518, -0.002949581024448342, -0.003449532389579513, -0.003608829916851618, -0.003398558758097474, +-0.002826619977722426, -0.001938296425724648, -0.000813026544735210, 0.000442428219163585, 0.001703859853903780, + 0.002841646272526556, 0.003733795631607910, 0.004278716742047143, 0.004406405339454943, 0.004086851240208961, + 0.003334703369161471, 0.002209574959936082, 0.000811784328342671, -0.000726216440571004, -0.002252031018207481, +-0.003608037091401704, -0.004647249273905110, -0.005248768326240763, -0.005331240183248048, -0.004862881229352157, +-0.003866915473379464, -0.002421678784759232, -0.000655151171295565, 0.001265777761167976, 0.003150462759634698, + 0.004802753890816374, 0.006040653489191535, 0.006715477432817280, 0.006728582425712883, 0.006043878643663059, + 0.004694681402232655, 0.002783951074178680, 0.000477576037627513, -0.002008976414542753, -0.004429653284750409, +-0.006530865070729735, -0.008076534379704757, -0.008872792811204462, -0.008789897699281934, -0.007779088447247421, +-0.005882478096374164, -0.003234645557614322, -0.000055318960121547, 0.003366648103157717, 0.006696974329608038, + 0.009585521827024799, 0.011699303862532495, 0.012756057470065627, 0.012555316826801740, 0.011004006112499458, + 0.008133925792679506, 0.004109116367295407, -0.000778103172178875, -0.006122670687011962, -0.011433128819157286, +-0.016167043119046529, -0.019772887723249422, -0.021735132723275978, -0.021618869254173001, -0.019110214094446909, +-0.014048964546840230, -0.006450511384232603, 0.003485174039118999, 0.015378649609086421, 0.028690243785521201, + 0.042752300360393465, 0.056812206812071039, 0.070083061994479681, 0.081798161497452851, 0.091265110934848792, + 0.097915351884672430, 0.101345208406692710, 0.101345208406692710, 0.097915351884672430, 0.091265110934848792, + 0.081798161497452851, 0.070083061994479681, 0.056812206812071039, 0.042752300360393465, 0.028690243785521201, + 0.015378649609086421, 0.003485174039118999, -0.006450511384232603, -0.014048964546840230, -0.019110214094446909, +-0.021618869254173001, -0.021735132723275978, -0.019772887723249422, -0.016167043119046529, -0.011433128819157286, +-0.006122670687011962, -0.000778103172178875, 0.004109116367295407, 0.008133925792679506, 0.011004006112499458, + 0.012555316826801740, 0.012756057470065627, 0.011699303862532495, 0.009585521827024799, 0.006696974329608038, + 0.003366648103157717, -0.000055318960121547, -0.003234645557614322, -0.005882478096374164, -0.007779088447247421, +-0.008789897699281934, -0.008872792811204462, -0.008076534379704757, -0.006530865070729735, -0.004429653284750409, +-0.002008976414542753, 0.000477576037627513, 0.002783951074178680, 0.004694681402232655, 0.006043878643663059, + 0.006728582425712883, 0.006715477432817280, 0.006040653489191535, 0.004802753890816374, 0.003150462759634698, + 0.001265777761167976, -0.000655151171295565, -0.002421678784759232, -0.003866915473379464, -0.004862881229352157, +-0.005331240183248048, -0.005248768326240763, -0.004647249273905110, -0.003608037091401704, -0.002252031018207481, +-0.000726216440571004, 0.000811784328342671, 0.002209574959936082, 0.003334703369161471, 0.004086851240208961, + 0.004406405339454943, 0.004278716742047143, 0.003733795631607910, 0.002841646272526556, 0.001703859853903780, + 0.000442428219163585, -0.000813026544735210, -0.001938296425724648, -0.002826619977722426, -0.003398558758097474, +-0.003608829916851618, -0.003449532389579513, -0.002949581024448342, -0.002170536435942518, -0.001199369993895262, +-0.000138980947793396, 0.000902520745944448, 0.001822671724082916, 0.002534577733743369, 0.002974959532860613, + 0.003109591237339856, 0.002935686794045762, 0.002481100933662590, 0.001800534718138737, 0.000969218460672265, + 0.000074782605168353, -0.000791823925944463, -0.001545437314172064, -0.002114908771525688, -0.002449675079358536, +-0.002524079501171732, -0.002339087231340391, -0.001921314886803284, -0.001319555291352399, -0.000599227139535745, + 0.000164637922390302, 0.000895127993320633, 0.001521121064322945, 0.001984133400353534, 0.002243699390601673, + 0.002280794678156941, 0.002099032072900320, 0.001723580332124720, 0.001197989750585395, 0.000579300435978082, +-0.000068024295664869, -0.000678706705799908, -0.001193131136885269, -0.001563058384174837, -0.001756034066558804, +-0.001758102209059370, -0.001574604672635160, -0.001229053244090960, -0.000760241586711499, -0.000217942303300231, + 0.000342348399080757, 0.000865065069005933, 0.001300061195708352, 0.001607388781175447, 0.001760927137558775, + 0.001750535054942567, 0.001582567444665569, 0.001278748950006680, 0.000873571510876301, 0.000410511052497376, +-0.000062520041312805, -0.000498020971159122, -0.000853559853825754, -0.001095797960360223, -0.001203497801923855, +-0.001169257920406941, -0.000999834598355945, -0.000715070316355716, -0.000345567500376679, 0.000070618732748754, + 0.000491924577078300, 0.000877443544384873, 0.001190910569750263, 0.001404113830652116, 0.001499431895309172, + 0.001471265194417923, 0.001326263088537886, 0.001082346669587512, 0.000766669398901066, 0.000412734842368625, + 0.000056986807722520, -0.000264798296929030, -0.000520952456912102, -0.000686876298204384, -0.000747233312787443, +-0.000697204883792079, -0.000542701008001189, -0.000299551487706380, 0.000008234786560396, 0.000350937369521106, + 0.000696080786323511, 0.001011568778937986, 0.001268714805496182, 0.001444870661224978, 0.001525437805536890, + 0.001505069987035192, 0.001388000318914036, 0.001187470307244314, 0.000924363583767082, 0.000625164037239141, + 0.000319460819342812, 0.000037212952694738, -0.000193939445451714, -0.000351100298847373, -0.000418046328997344, +-0.000386544706976665, -0.000256935860609037, -0.000037984405595945, 0.000254003709475283, 0.000596656779876184, + 0.000963492246325779, 0.001326124950893504, 0.001656578667677397, 0.001929455560771270, 0.002123834204231122, + 0.002224702189457267, 0.002223894227313585, 0.002120431736364362, 0.001920322587244723, 0.001635786964228304, + 0.001284080850383070, 0.000885978417045319, 0.000464151153253057, 0.000041472210381792, -0.000360540808884225, +-0.000723138798454071, -0.001031365182705646, -0.001274757191818737, -0.001447577149496593, -0.001548788902291486, +-0.001581622501567850, -0.001552963425946309, -0.001472389090721280, -0.001351260760102592, -0.001201670675683178, +-0.001035675409027330, -0.000864361814542882, -0.000697354821092750, -0.000542187350923721, -0.000404455107555149, +-0.000287398500897765, -0.000192494530734137, -0.000119178480453066, -0.000087601196745517} ; + +// Sample 144 kHz, pass 20.0, stop 24.0, ripple 0.1dB, atten 100 dB. Pass 0.138889. Stop 0.166667. Decimate by 3. +double quiskFilt144D3Coefs[147] = { + 0.000016780987039824, 0.000019929721906467, -0.000029812334087366, -0.000185957342855632, -0.000457595041742833, -0.000763020582249048, +-0.000935502330460281, -0.000810648972696192, -0.000358803671633800, 0.000234868974461967, 0.000635518410854574, 0.000568649052653316, + 0.000046290780734364, -0.000576848135417221, -0.000812569262690978, -0.000406382962866972, 0.000409530745262834, 0.001018717022433781, + 0.000868514981759032, -0.000047834218816234, -0.001083548438467626, -0.001372913494385146, -0.000536984649454124, 0.000901238288475838, + 0.001808128325743618, 0.001313343518974492, -0.000390643142762094, -0.002034153793379463, -0.002190363787369149, -0.000486785830211702, + 0.001899971815554939, 0.003016393051189844, 0.001703802959800975, -0.001272713184907955, -0.003592060794645169, -0.003153839927509408, + 0.000067273868535848, 0.003691997080030787, 0.004646339876200064, 0.001725059542379199, -0.003095000160661851, -0.005913611355699069, +-0.004013555322861903, 0.001618893140735101, 0.006628860694469696, 0.006593761434451985, 0.000843918175071128, -0.006434294798483291, +-0.009145937757863428, -0.004298379230455153, 0.004970723844275416, 0.011242560431712569, 0.008630896022305587, -0.001897430256163395, +-0.012354817079092976, -0.013606356255629011, -0.003111995792617545, 0.011842459244895697, 0.018885015598445000, 0.010442018188565504, +-0.008869010528864909, -0.024054103081211958, -0.020781522872984903, 0.002077600796052109, 0.028673899750479920, 0.035969215939495834, + 0.011698610672543283, -0.032323400359080219, -0.062850340377290967, -0.044670144014306225, 0.034665193983578813, 0.151715188318120200, + 0.256129260910589850, 0.297861169542637920, 0.256129260910589850, 0.151715188318120200, 0.034665193983578813, -0.044670144014306225, +-0.062850340377290967, -0.032323400359080219, 0.011698610672543283, 0.035969215939495834, 0.028673899750479920, 0.002077600796052109, +-0.020781522872984903, -0.024054103081211958, -0.008869010528864909, 0.010442018188565504, 0.018885015598445000, 0.011842459244895697, +-0.003111995792617545, -0.013606356255629011, -0.012354817079092976, -0.001897430256163395, 0.008630896022305587, 0.011242560431712569, + 0.004970723844275416, -0.004298379230455153, -0.009145937757863428, -0.006434294798483291, 0.000843918175071128, 0.006593761434451985, + 0.006628860694469696, 0.001618893140735101, -0.004013555322861903, -0.005913611355699069, -0.003095000160661851, 0.001725059542379199, + 0.004646339876200064, 0.003691997080030787, 0.000067273868535848, -0.003153839927509408, -0.003592060794645169, -0.001272713184907955, + 0.001703802959800975, 0.003016393051189844, 0.001899971815554939, -0.000486785830211702, -0.002190363787369149, -0.002034153793379463, +-0.000390643142762094, 0.001313343518974492, 0.001808128325743618, 0.000901238288475838, -0.000536984649454124, -0.001372913494385146, +-0.001083548438467626, -0.000047834218816234, 0.000868514981759032, 0.001018717022433781, 0.000409530745262834, -0.000406382962866972, +-0.000812569262690978, -0.000576848135417221, 0.000046290780734364, 0.000568649052653316, 0.000635518410854574, 0.000234868974461967, +-0.000358803671633800, -0.000810648972696192, -0.000935502330460281, -0.000763020582249048, -0.000457595041742833, -0.000185957342855632, +-0.000029812334087366, 0.000019929721906467, 0.000016780987039824 +} ; + +// Sample 120000 Hz, pass 2700, stop 3730, ripple 0.1dB, atten 100 dB. Stop 0.03108. +double quiskFilt120s03[480] = { -0.000005050567303837, -0.000000267011791999, 0.000000197734700398, 0.000001038946634000, + 0.000002322193058869, 0.000004115682735322, 0.000006499942123311, 0.000009551098482930, 0.000013350669444763, + 0.000017966192635412, 0.000023463361155584, 0.000029885221425020, 0.000037271082107518, 0.000045630720487935, + 0.000054970017069384, 0.000065233162392019, 0.000076360900545177, 0.000088271373315159, 0.000100818605854714, + 0.000113853476544409, 0.000127174196746337, 0.000140558396336177, 0.000153744508371709, 0.000166450784469067, + 0.000178368313347299, 0.000189176709991702, 0.000198541881389953, 0.000206128795372885, 0.000211604878787747, + 0.000214655997661182, 0.000214994859281552, 0.000212358734245594, 0.000206539880117977, 0.000197379393194548, + 0.000184780318878738, 0.000168719942655099, 0.000149250512353807, 0.000126511346757621, 0.000100726393185629, + 0.000072210925236429, 0.000041365841965015, 0.000008680571408025, -0.000025277165852799, -0.000059865389594949, +-0.000094384355854646, -0.000128080670195777, -0.000160170174848483, -0.000189854272533545, -0.000216333899003825, +-0.000238836419299503, -0.000256632149501508, -0.000269058714331757, -0.000275541485292432, -0.000275614059005332, +-0.000268937472718753, -0.000255317038867589, -0.000234717772155001, -0.000207273956099563, -0.000173297342436372, +-0.000133280012107173, -0.000087895370243821, -0.000037986085678081, 0.000015440388211825, 0.000071232572821451, + 0.000128114399130489, 0.000184710477990398, 0.000239577162514028, 0.000291234779803098, 0.000338204791740229, + 0.000379047713684221, 0.000412403761615261, 0.000437031818051652, 0.000451848709179591, 0.000455966225408344, + 0.000448726371643413, 0.000429729020814434, 0.000398857326863837, 0.000356297600912998, 0.000302547334727027, + 0.000238422248479072, 0.000165048886226905, 0.000083853091464077, -0.000003462782744354, -0.000094949813106744, +-0.000188451833293202, -0.000281651282503015, -0.000372121907291206, -0.000457387566635848, -0.000534985542936898, +-0.000602532044011899, -0.000657788245032425, -0.000698728981427767, -0.000723604675185869, -0.000731002305621048, +-0.000719899536922384, -0.000689709694056092, -0.000640319946685634, -0.000572115873292030, -0.000485996080304965, +-0.000383371840261246, -0.000266155252511831, -0.000136731311264191, 0.000002082667095075, 0.000147092077716480, + 0.000294790953130229, 0.000441441918072383, 0.000583164190168290, 0.000716029226064227, 0.000836164238172957, + 0.000939856052624227, 0.001023657909064450, 0.001084492755093968, 0.001119751426837743, 0.001127383039339373, + 0.001105974243787613, 0.001054815583369999, 0.000973950761085690, 0.000864209315714227, 0.000727219011746881, + 0.000565398080608305, 0.000381924396468366, 0.000180685902835315, -0.000033793183292569, -0.000256444114966522, +-0.000481764526566339, -0.000703946352348464, -0.000917016099829735, -0.001114986581270253, -0.001292014799874503, +-0.001442563411804926, -0.001561559957317790, -0.001644551048567398, -0.001687846581475964, -0.001688649703502788, +-0.001645167889846890, -0.001556702802350076, -0.001423714708648073, -0.001247857669697092, -0.001031986722557201, +-0.000780131048444402, -0.000497436825078657, -0.000190077210351809, 0.000134868279325909, 0.000469563533327739, + 0.000805591531546815, 0.001134152328775355, 0.001446279849797673, 0.001733071409562941, 0.001985924997799762, + 0.002196778054604388, 0.002358342626407065, 0.002464328098407475, 0.002509648218888532, 0.002490604086803692, + 0.002405037734357425, 0.002252452724297770, 0.002034094661603120, 0.001752990365583534, 0.001413941154886139, + 0.001023470495638453, 0.000589723521647734, 0.000122320866350319, -0.000367832138027160, -0.000868777013398284, +-0.001367771151677059, -0.001851587344265625, -0.002306838088978190, -0.002720317947026380, -0.003079353614002113, +-0.003372155891804708, -0.003588162376578369, -0.003718362558663737, -0.003755596511143005, -0.003694818131674599, +-0.003533315298404129, -0.003270878754553819, -0.002909914962857412, -0.002455496391464944, -0.001915346645364514, +-0.001299757227227888, -0.000621437066532776, 0.000104706515738248, 0.000861849931067767, 0.001631595707499856, + 0.002394368911341672, 0.003129858565588139, 0.003817496679992245, 0.004436963307209760, 0.004968707287606522, + 0.005394469536085115, 0.005697797543539088, 0.005864537618023589, 0.005883292537600076, 0.005745832319314692, + 0.005447447099071761, 0.004987231255534477, 0.004368289529377007, 0.003597859022418248, 0.002687338851256991, + 0.001652226293162047, 0.000511956075882180, -0.000710356149138656, -0.001988263330091648, -0.003292424566049982, +-0.004591123342747130, -0.005850857852106148, -0.007036991266043732, -0.008114450164977267, -0.009048456200082230, +-0.009805276478965942, -0.010352975302354198, -0.010662152577592631, -0.010706650669328861, -0.010464214075017983, +-0.009917087295446811, -0.009052534679222271, -0.007863270920348924, -0.006347789704693751, -0.004510582323649121, +-0.002362238055733795, 0.000080576968834213, 0.002795265196543707, 0.005753566158586979, 0.008921944932552510, + 0.012262093950265378, 0.015731539846483594, 0.019284344624007944, 0.022871886384520687, 0.026443706729191677, + 0.029948406200633094, 0.033334570666910354, 0.036551709955124537, 0.039551189200810140, 0.042287133974308874, + 0.044717290029466283, 0.046803820535016104, 0.048514022996355009, 0.049820951883635139, 0.050703932928426454, + 0.051148959210315710, 0.051148959210315710, 0.050703932928426454, 0.049820951883635139, 0.048514022996355009, + 0.046803820535016104, 0.044717290029466283, 0.042287133974308874, 0.039551189200810140, 0.036551709955124537, + 0.033334570666910354, 0.029948406200633094, 0.026443706729191677, 0.022871886384520687, 0.019284344624007944, + 0.015731539846483594, 0.012262093950265378, 0.008921944932552510, 0.005753566158586979, 0.002795265196543707, + 0.000080576968834213, -0.002362238055733795, -0.004510582323649121, -0.006347789704693751, -0.007863270920348924, +-0.009052534679222271, -0.009917087295446811, -0.010464214075017983, -0.010706650669328861, -0.010662152577592631, +-0.010352975302354198, -0.009805276478965942, -0.009048456200082230, -0.008114450164977267, -0.007036991266043732, +-0.005850857852106148, -0.004591123342747130, -0.003292424566049982, -0.001988263330091648, -0.000710356149138656, + 0.000511956075882180, 0.001652226293162047, 0.002687338851256991, 0.003597859022418248, 0.004368289529377007, + 0.004987231255534477, 0.005447447099071761, 0.005745832319314692, 0.005883292537600076, 0.005864537618023589, + 0.005697797543539088, 0.005394469536085115, 0.004968707287606522, 0.004436963307209760, 0.003817496679992245, + 0.003129858565588139, 0.002394368911341672, 0.001631595707499856, 0.000861849931067767, 0.000104706515738248, +-0.000621437066532776, -0.001299757227227888, -0.001915346645364514, -0.002455496391464944, -0.002909914962857412, +-0.003270878754553819, -0.003533315298404129, -0.003694818131674599, -0.003755596511143005, -0.003718362558663737, +-0.003588162376578369, -0.003372155891804708, -0.003079353614002113, -0.002720317947026380, -0.002306838088978190, +-0.001851587344265625, -0.001367771151677059, -0.000868777013398284, -0.000367832138027160, 0.000122320866350319, + 0.000589723521647734, 0.001023470495638453, 0.001413941154886139, 0.001752990365583534, 0.002034094661603120, + 0.002252452724297770, 0.002405037734357425, 0.002490604086803692, 0.002509648218888532, 0.002464328098407475, + 0.002358342626407065, 0.002196778054604388, 0.001985924997799762, 0.001733071409562941, 0.001446279849797673, + 0.001134152328775355, 0.000805591531546815, 0.000469563533327739, 0.000134868279325909, -0.000190077210351809, +-0.000497436825078657, -0.000780131048444402, -0.001031986722557201, -0.001247857669697092, -0.001423714708648073, +-0.001556702802350076, -0.001645167889846890, -0.001688649703502788, -0.001687846581475964, -0.001644551048567398, +-0.001561559957317790, -0.001442563411804926, -0.001292014799874503, -0.001114986581270253, -0.000917016099829735, +-0.000703946352348464, -0.000481764526566339, -0.000256444114966522, -0.000033793183292569, 0.000180685902835315, + 0.000381924396468366, 0.000565398080608305, 0.000727219011746881, 0.000864209315714227, 0.000973950761085690, + 0.001054815583369999, 0.001105974243787613, 0.001127383039339373, 0.001119751426837743, 0.001084492755093968, + 0.001023657909064450, 0.000939856052624227, 0.000836164238172957, 0.000716029226064227, 0.000583164190168290, + 0.000441441918072383, 0.000294790953130229, 0.000147092077716480, 0.000002082667095075, -0.000136731311264191, +-0.000266155252511831, -0.000383371840261246, -0.000485996080304965, -0.000572115873292030, -0.000640319946685634, +-0.000689709694056092, -0.000719899536922384, -0.000731002305621048, -0.000723604675185869, -0.000698728981427767, +-0.000657788245032425, -0.000602532044011899, -0.000534985542936898, -0.000457387566635848, -0.000372121907291206, +-0.000281651282503015, -0.000188451833293202, -0.000094949813106744, -0.000003462782744354, 0.000083853091464077, + 0.000165048886226905, 0.000238422248479072, 0.000302547334727027, 0.000356297600912998, 0.000398857326863837, + 0.000429729020814434, 0.000448726371643413, 0.000455966225408344, 0.000451848709179591, 0.000437031818051652, + 0.000412403761615261, 0.000379047713684221, 0.000338204791740229, 0.000291234779803098, 0.000239577162514028, + 0.000184710477990398, 0.000128114399130489, 0.000071232572821451, 0.000015440388211825, -0.000037986085678081, +-0.000087895370243821, -0.000133280012107173, -0.000173297342436372, -0.000207273956099563, -0.000234717772155001, +-0.000255317038867589, -0.000268937472718753, -0.000275614059005332, -0.000275541485292432, -0.000269058714331757, +-0.000256632149501508, -0.000238836419299503, -0.000216333899003825, -0.000189854272533545, -0.000160170174848483, +-0.000128080670195777, -0.000094384355854646, -0.000059865389594949, -0.000025277165852799, 0.000008680571408025, + 0.000041365841965015, 0.000072210925236429, 0.000100726393185629, 0.000126511346757621, 0.000149250512353807, + 0.000168719942655099, 0.000184780318878738, 0.000197379393194548, 0.000206539880117977, 0.000212358734245594, + 0.000214994859281552, 0.000214655997661182, 0.000211604878787747, 0.000206128795372885, 0.000198541881389953, + 0.000189176709991702, 0.000178368313347299, 0.000166450784469067, 0.000153744508371709, 0.000140558396336177, + 0.000127174196746337, 0.000113853476544409, 0.000100818605854714, 0.000088271373315159, 0.000076360900545177, + 0.000065233162392019, 0.000054970017069384, 0.000045630720487935, 0.000037271082107518, 0.000029885221425020, + 0.000023463361155584, 0.000017966192635412, 0.000013350669444763, 0.000009551098482930, 0.000006499942123311, + 0.000004115682735322, 0.000002322193058869, 0.000001038946634000, 0.000000197734700398, -0.000000267011791999, +-0.000005050567303837 }; + +// Sample 9600 kHz, pass 168, stop 216, ripple 0.1dB, atten 100 dB. Stop 0.0225. +double quiskFiltI3D25Coefs[825] = { 0.000005956787452187, 0.000003211705784635, 0.000004034862817142, 0.000004961356794279, + 0.000005992480684896, 0.000007125011282928, 0.000008356176996335, 0.000009678674512241, 0.000011085681450866, 0.000012565356814181, + 0.000014105577722777, 0.000015688094611533, 0.000017293987798534, 0.000018898513368975, 0.000020477970947099, 0.000022003882695500, + 0.000023449026651569, 0.000024779370194270, 0.000025960975613092, 0.000026954037148028, 0.000027724129005342, 0.000028233620872516, + 0.000028448168208646, 0.000028322403611347, 0.000027819244560919, 0.000026904364434149, 0.000025555768460688, 0.000023718858672597, + 0.000021388236737303, 0.000018531461504295, 0.000015128617900530, 0.000011172338433941, 0.000006650682808194, 0.000001567980147682, +-0.000004071844887927, -0.000010251946885753, -0.000016952037416704, -0.000024139403703601, -0.000031775674847193, -0.000039811520245419, +-0.000048192148911461, -0.000056852971113359, -0.000065722611018575, -0.000074720205221454, -0.000083759727095511, -0.000092749188319604, +-0.000101592108032084, -0.000110185878000537, -0.000118425032569519, -0.000126204064875885, -0.000133416362597982, -0.000139953974896786, +-0.000145712082126712, -0.000150594676074443, -0.000154502739904809, -0.000157349878306252, -0.000159058476336748, -0.000159555772217867, +-0.000158788109935490, -0.000156707360410909, -0.000153284964905293, -0.000148502970885125, -0.000142363405313636, -0.000134882362974756, +-0.000126095843217766, -0.000116055019098236, -0.000104831203935920, -0.000092512000120117, -0.000079204203578124, -0.000065029325935631, +-0.000050126352194410, -0.000034647988869857, -0.000018761566915687, -0.000002643867645482, 0.000013517370747292, 0.000029526982476978, + 0.000045184046634717, 0.000060287289500742, 0.000074633464306207, 0.000088022237529899, 0.000100260387268166, 0.000111162183255553, + 0.000120552183081292, 0.000128273257824200, 0.000134179871751258, 0.000138151310658245, 0.000140084636985629, 0.000139904660813329, + 0.000137559905089999, 0.000133028662869780, 0.000126316758019544, 0.000117461929793833, 0.000106531164007165, 0.000093623364714925, + 0.000078866698566106, 0.000062420736245776, 0.000044472363393569, 0.000025236122848728, 0.000004949997449017, -0.000016124117496096, +-0.000037706322052971, -0.000059500304426378, -0.000081198420104433, -0.000102483313141189, -0.000123035628711338, -0.000142535319778232, +-0.000160667780360279, -0.000177128235455148, -0.000191627620690743, -0.000203893426905360, -0.000213680605929445, -0.000220768612480065, +-0.000224972129527046, -0.000226139442542857, -0.000224159617516556, -0.000218962999255533, -0.000210525248496711, -0.000198867299743239, +-0.000184058116066018, -0.000166214040676592, -0.000145499890142001, -0.000122126241762541, -0.000096349500708075, -0.000068468267762019, +-0.000038821745842297, -0.000007783989333822, 0.000024238365952557, 0.000056812590122291, 0.000089484319755483, 0.000121785090745373, + 0.000153236391205647, 0.000183359023230085, 0.000211678221012634, 0.000237731645367386, 0.000261075639702519, 0.000281294681250729, + 0.000298004342402298, 0.000310862516511249, 0.000319571315012658, 0.000323886296653843, 0.000323619563118675, 0.000318644983901538, + 0.000308901513454268, 0.000294396418133417, 0.000275206841101241, 0.000251480887099541, 0.000223437061305126, 0.000191363947212061, + 0.000155617045621414, 0.000116616164620783, 0.000074840231468239, 0.000030823184154666, -0.000014853764966253, -0.000061568272389658, +-0.000108665640335580, -0.000155466780716653, -0.000201278972426392, -0.000245403805143397, -0.000287148930939394, -0.000325837733962965, +-0.000360820345491574, -0.000391482737253097, -0.000417258980581691, -0.000437638220339685, -0.000452176524646696, -0.000460503187462496, +-0.000462329168633824, -0.000457453739285960, -0.000445769357864568, -0.000427266209021117, -0.000402034633782136, -0.000370266824699425, +-0.000332256748568874, -0.000288398092983417, -0.000239181635064980, -0.000185190057199916, -0.000127092351239062, -0.000065635351677973, +-0.000001635768489257, 0.000064031032789795, 0.000130441926615597, 0.000196638895244753, 0.000261641460484809, 0.000324462014025233, + 0.000384119062216776, 0.000439652650855628, 0.000490138798319519, 0.000534704796699404, 0.000572542368876905, 0.000602922923466479, + 0.000625208686170919, 0.000638865887614407, 0.000643474954073381, 0.000638739227035361, 0.000624493258921266, 0.000600707953030737, + 0.000567495007786956, 0.000525108546609422, 0.000473944885437249, 0.000414540661406180, 0.000347567762721358, 0.000273827417009147, + 0.000194240866662439, 0.000109839511430035, 0.000021751281014785, -0.000068812726758276, -0.000160574821442717, -0.000252207643694647, +-0.000342353358610352, -0.000429641983841077, -0.000512712588279566, -0.000590233000458980, -0.000660920833710300, -0.000723563310391309, +-0.000777037688231366, -0.000820329138707098, -0.000852549805275905, -0.000872954045876351, -0.000880953408348413, -0.000876129480105976, +-0.000858243707443438, -0.000827245823239233, -0.000783278920419950, -0.000726681966249080, -0.000657989929078930, -0.000577929796159890, +-0.000487415411907342, -0.000387537349196916, -0.000279551948319666, -0.000164865572977560, -0.000045018545421742, 0.000078335772017527, + 0.000203451977405086, 0.000328516992293320, 0.000451674636902284, 0.000571053266050113, 0.000684792094554321, 0.000791070116123563, + 0.000888133372727545, 0.000974323218404695, 0.001048102631468540, 0.001108082857136429, 0.001153046414612510, 0.001181970380085016, + 0.001194045545471417, 0.001188693681255879, 0.001165581726339971, 0.001124632477690236, 0.001066031623671160, 0.000990231761272084, + 0.000897951111087878, 0.000790170354647780, 0.000668122820361005, 0.000533283635452978, 0.000387352203340500, 0.000232233621309843, + 0.000070013789254570, -0.000097065849154538, -0.000266639011630557, -0.000436245878669300, -0.000603368261275017, -0.000765464501726597, +-0.000920007831966061, -0.001064523015142339, -0.001196625296357533, -0.001314057093625971, -0.001414725206513849, -0.001496734827620223, +-0.001558423502125369, -0.001598390052570974, -0.001615522526442618, -0.001609020914211984, -0.001578416762563850, -0.001523587643370717, +-0.001444767829815729, -0.001342552379906827, -0.001217898207865990, -0.001072116979449732, -0.000906866009268772, -0.000724130257712046, +-0.000526202440125150, -0.000315655017075901, -0.000095310483896663, 0.000131795797362412, 0.000362454044567802, 0.000593324198824254, + 0.000820981122794087, 0.001041964098270290, 0.001252825798400345, 0.001450184706684350, 0.001630775350917507, 0.001791500206060708, + 0.001929478553733191, 0.002042095133402677, 0.002127043989719768, 0.002182371279443224, 0.002206511463226233, 0.002198320858196307, + 0.002157104185387105, 0.002082636609166360, 0.001975177863452211, 0.001835481726247019, 0.001664795879174717, 0.001464857801720407, + 0.001237879727116529, 0.000986530138212679, 0.000713904673185216, 0.000423493241763419, 0.000119137628223145, -0.000195014201816123, +-0.000514561915304833, -0.000834909513284288, -0.001151328068004269, -0.001459020234601911, -0.001753189335167306, -0.002029107895048567, +-0.002282189352141815, -0.002508057311799787, -0.002702615502811452, -0.002862113710271713, -0.002983212400539942, -0.003063040853327989, +-0.003099252599280703, -0.003090072441393503, -0.003034338569084416, -0.002931535299727895, -0.002781819336183325, -0.002586034995156839, +-0.002345722740096574, -0.002063114928211339, -0.001741125267586707, -0.001383324779597604, -0.000993911106418735, -0.000577665400414888, +-0.000139902377919259, 0.000313590758783057, 0.000776621606795215, 0.001242669040263383, 0.001704964962659825, 0.002156583049235837, + 0.002590530627198782, 0.002999845936290586, 0.003377695514868402, 0.003717474718472581, 0.004012906048525476, 0.004258137513089033, + 0.004447836307400812, 0.004577280134783838, 0.004642440654159172, 0.004640062324902708, 0.004567731152875479, 0.004423936073887462, + 0.004208118356932086, 0.003920711899156275, 0.003563169368207032, 0.003137978634478423, 0.002648663135226872, 0.002099772065464075, + 0.001496854451285576, 0.000846422020958481, 0.000155897036473891, -0.000566451115784383, -0.001311583033678882, -0.002069776837875344, +-0.002830727368610444, -0.003583654241941997, -0.004317419512253912, -0.005020651106998095, -0.005681873967977548, -0.006289643577342038, +-0.006832684074428672, -0.007300025943397813, -0.007681144614133184, -0.007966095174260545, -0.008145645068345550, -0.008211399133170209, +-0.008155919326273169, -0.007972833886033631, -0.007656937873393390, -0.007204280391407546, -0.006612240797414353, -0.005879588674156967, +-0.005006531452725929, -0.003994743792822069, -0.002847382842898682, -0.001569085180199714, -0.000165948105466916, 0.001354506897970326, + 0.002983386500934207, 0.004710494837761955, 0.006524417826120845, 0.008412622862724265, 0.010361571520526983, 0.012356846115947544, + 0.014383285875718958, 0.016425134848742497, 0.018466196154810609, 0.020489994296242191, 0.022479940752782465, 0.024419503808109731, + 0.026292377563203666, 0.028082651706093722, 0.029774976416515504, 0.031354724068567270, 0.032808142535659224, 0.034122501353439978, + 0.035286226053449159, 0.036289022437609730, 0.037121985402646829, 0.037777695264168000, 0.038250296541390075, 0.038535561348862886, + 0.038630934515923344, 0.038535561348862886, 0.038250296541390075, 0.037777695264168000, 0.037121985402646829, 0.036289022437609730, + 0.035286226053449159, 0.034122501353439978, 0.032808142535659224, 0.031354724068567270, 0.029774976416515504, 0.028082651706093722, + 0.026292377563203666, 0.024419503808109731, 0.022479940752782465, 0.020489994296242191, 0.018466196154810609, 0.016425134848742497, + 0.014383285875718958, 0.012356846115947544, 0.010361571520526983, 0.008412622862724265, 0.006524417826120845, 0.004710494837761955, + 0.002983386500934207, 0.001354506897970326, -0.000165948105466916, -0.001569085180199714, -0.002847382842898682, -0.003994743792822069, +-0.005006531452725929, -0.005879588674156967, -0.006612240797414353, -0.007204280391407546, -0.007656937873393390, -0.007972833886033631, +-0.008155919326273169, -0.008211399133170209, -0.008145645068345550, -0.007966095174260545, -0.007681144614133184, -0.007300025943397813, +-0.006832684074428672, -0.006289643577342038, -0.005681873967977548, -0.005020651106998095, -0.004317419512253912, -0.003583654241941997, +-0.002830727368610444, -0.002069776837875344, -0.001311583033678882, -0.000566451115784383, 0.000155897036473891, 0.000846422020958481, + 0.001496854451285576, 0.002099772065464075, 0.002648663135226872, 0.003137978634478423, 0.003563169368207032, 0.003920711899156275, + 0.004208118356932086, 0.004423936073887462, 0.004567731152875479, 0.004640062324902708, 0.004642440654159172, 0.004577280134783838, + 0.004447836307400812, 0.004258137513089033, 0.004012906048525476, 0.003717474718472581, 0.003377695514868402, 0.002999845936290586, + 0.002590530627198782, 0.002156583049235837, 0.001704964962659825, 0.001242669040263383, 0.000776621606795215, 0.000313590758783057, +-0.000139902377919259, -0.000577665400414888, -0.000993911106418735, -0.001383324779597604, -0.001741125267586707, -0.002063114928211339, +-0.002345722740096574, -0.002586034995156839, -0.002781819336183325, -0.002931535299727895, -0.003034338569084416, -0.003090072441393503, +-0.003099252599280703, -0.003063040853327989, -0.002983212400539942, -0.002862113710271713, -0.002702615502811452, -0.002508057311799787, +-0.002282189352141815, -0.002029107895048567, -0.001753189335167306, -0.001459020234601911, -0.001151328068004269, -0.000834909513284288, +-0.000514561915304833, -0.000195014201816123, 0.000119137628223145, 0.000423493241763419, 0.000713904673185216, 0.000986530138212679, + 0.001237879727116529, 0.001464857801720407, 0.001664795879174717, 0.001835481726247019, 0.001975177863452211, 0.002082636609166360, + 0.002157104185387105, 0.002198320858196307, 0.002206511463226233, 0.002182371279443224, 0.002127043989719768, 0.002042095133402677, + 0.001929478553733191, 0.001791500206060708, 0.001630775350917507, 0.001450184706684350, 0.001252825798400345, 0.001041964098270290, + 0.000820981122794087, 0.000593324198824254, 0.000362454044567802, 0.000131795797362412, -0.000095310483896663, -0.000315655017075901, +-0.000526202440125150, -0.000724130257712046, -0.000906866009268772, -0.001072116979449732, -0.001217898207865990, -0.001342552379906827, +-0.001444767829815729, -0.001523587643370717, -0.001578416762563850, -0.001609020914211984, -0.001615522526442618, -0.001598390052570974, +-0.001558423502125369, -0.001496734827620223, -0.001414725206513849, -0.001314057093625971, -0.001196625296357533, -0.001064523015142339, +-0.000920007831966061, -0.000765464501726597, -0.000603368261275017, -0.000436245878669300, -0.000266639011630557, -0.000097065849154538, + 0.000070013789254570, 0.000232233621309843, 0.000387352203340500, 0.000533283635452978, 0.000668122820361005, 0.000790170354647780, + 0.000897951111087878, 0.000990231761272084, 0.001066031623671160, 0.001124632477690236, 0.001165581726339971, 0.001188693681255879, + 0.001194045545471417, 0.001181970380085016, 0.001153046414612510, 0.001108082857136429, 0.001048102631468540, 0.000974323218404695, + 0.000888133372727545, 0.000791070116123563, 0.000684792094554321, 0.000571053266050113, 0.000451674636902284, 0.000328516992293320, + 0.000203451977405086, 0.000078335772017527, -0.000045018545421742, -0.000164865572977560, -0.000279551948319666, -0.000387537349196916, +-0.000487415411907342, -0.000577929796159890, -0.000657989929078930, -0.000726681966249080, -0.000783278920419950, -0.000827245823239233, +-0.000858243707443438, -0.000876129480105976, -0.000880953408348413, -0.000872954045876351, -0.000852549805275905, -0.000820329138707098, +-0.000777037688231366, -0.000723563310391309, -0.000660920833710300, -0.000590233000458980, -0.000512712588279566, -0.000429641983841077, +-0.000342353358610352, -0.000252207643694647, -0.000160574821442717, -0.000068812726758276, 0.000021751281014785, 0.000109839511430035, + 0.000194240866662439, 0.000273827417009147, 0.000347567762721358, 0.000414540661406180, 0.000473944885437249, 0.000525108546609422, + 0.000567495007786956, 0.000600707953030737, 0.000624493258921266, 0.000638739227035361, 0.000643474954073381, 0.000638865887614407, + 0.000625208686170919, 0.000602922923466479, 0.000572542368876905, 0.000534704796699404, 0.000490138798319519, 0.000439652650855628, + 0.000384119062216776, 0.000324462014025233, 0.000261641460484809, 0.000196638895244753, 0.000130441926615597, 0.000064031032789795, +-0.000001635768489257, -0.000065635351677973, -0.000127092351239062, -0.000185190057199916, -0.000239181635064980, -0.000288398092983417, +-0.000332256748568874, -0.000370266824699425, -0.000402034633782136, -0.000427266209021117, -0.000445769357864568, -0.000457453739285960, +-0.000462329168633824, -0.000460503187462496, -0.000452176524646696, -0.000437638220339685, -0.000417258980581691, -0.000391482737253097, +-0.000360820345491574, -0.000325837733962965, -0.000287148930939394, -0.000245403805143397, -0.000201278972426392, -0.000155466780716653, +-0.000108665640335580, -0.000061568272389658, -0.000014853764966253, 0.000030823184154666, 0.000074840231468239, 0.000116616164620783, + 0.000155617045621414, 0.000191363947212061, 0.000223437061305126, 0.000251480887099541, 0.000275206841101241, 0.000294396418133417, + 0.000308901513454268, 0.000318644983901538, 0.000323619563118675, 0.000323886296653843, 0.000319571315012658, 0.000310862516511249, + 0.000298004342402298, 0.000281294681250729, 0.000261075639702519, 0.000237731645367386, 0.000211678221012634, 0.000183359023230085, + 0.000153236391205647, 0.000121785090745373, 0.000089484319755483, 0.000056812590122291, 0.000024238365952557, -0.000007783989333822, +-0.000038821745842297, -0.000068468267762019, -0.000096349500708075, -0.000122126241762541, -0.000145499890142001, -0.000166214040676592, +-0.000184058116066018, -0.000198867299743239, -0.000210525248496711, -0.000218962999255533, -0.000224159617516556, -0.000226139442542857, +-0.000224972129527046, -0.000220768612480065, -0.000213680605929445, -0.000203893426905360, -0.000191627620690743, -0.000177128235455148, +-0.000160667780360279, -0.000142535319778232, -0.000123035628711338, -0.000102483313141189, -0.000081198420104433, -0.000059500304426378, +-0.000037706322052971, -0.000016124117496096, 0.000004949997449017, 0.000025236122848728, 0.000044472363393569, 0.000062420736245776, + 0.000078866698566106, 0.000093623364714925, 0.000106531164007165, 0.000117461929793833, 0.000126316758019544, 0.000133028662869780, + 0.000137559905089999, 0.000139904660813329, 0.000140084636985629, 0.000138151310658245, 0.000134179871751258, 0.000128273257824200, + 0.000120552183081292, 0.000111162183255553, 0.000100260387268166, 0.000088022237529899, 0.000074633464306207, 0.000060287289500742, + 0.000045184046634717, 0.000029526982476978, 0.000013517370747292, -0.000002643867645482, -0.000018761566915687, -0.000034647988869857, +-0.000050126352194410, -0.000065029325935631, -0.000079204203578124, -0.000092512000120117, -0.000104831203935920, -0.000116055019098236, +-0.000126095843217766, -0.000134882362974756, -0.000142363405313636, -0.000148502970885125, -0.000153284964905293, -0.000156707360410909, +-0.000158788109935490, -0.000159555772217867, -0.000159058476336748, -0.000157349878306252, -0.000154502739904809, -0.000150594676074443, +-0.000145712082126712, -0.000139953974896786, -0.000133416362597982, -0.000126204064875885, -0.000118425032569519, -0.000110185878000537, +-0.000101592108032084, -0.000092749188319604, -0.000083759727095511, -0.000074720205221454, -0.000065722611018575, -0.000056852971113359, +-0.000048192148911461, -0.000039811520245419, -0.000031775674847193, -0.000024139403703601, -0.000016952037416704, -0.000010251946885753, +-0.000004071844887927, 0.000001567980147682, 0.000006650682808194, 0.000011172338433941, 0.000015128617900530, 0.000018531461504295, + 0.000021388236737303, 0.000023718858672597, 0.000025555768460688, 0.000026904364434149, 0.000027819244560919, 0.000028322403611347, + 0.000028448168208646, 0.000028233620872516, 0.000027724129005342, 0.000026954037148028, 0.000025960975613092, 0.000024779370194270, + 0.000023449026651569, 0.000022003882695500, 0.000020477970947099, 0.000018898513368975, 0.000017293987798534, 0.000015688094611533, + 0.000014105577722777, 0.000012565356814181, 0.000011085681450866, 0.000009678674512241, 0.000008356176996335, 0.000007125011282928, + 0.000005992480684896, 0.000004961356794279, 0.000004034862817142, 0.000003211705784635, 0.000005956787452187} ; + +// Sample 48000 Hz, pass 1350, stop 1650, ripple 0.2 dB, atten 80 dB. Stop 0.034375. +double quiskDgtFilt48Coefs[520] = { + 0.000061210247545999, 0.000043746409775174, 0.000058212225303250, 0.000074753467634881, 0.000093266105141811, 0.000113585644687065, + 0.000135423663054955, 0.000158328870775841, 0.000181891356788712, 0.000205599601545496, 0.000228645026826478, 0.000250549753299610, + 0.000270439656117594, 0.000287634273171333, 0.000301382803316157, 0.000310952112247671, 0.000315671378571221, 0.000314971116764541, + 0.000308384566541330, 0.000295546908455563, 0.000276344800843569, 0.000250714121608637, 0.000218894247891487, 0.000181247345217867, + 0.000138379308460797, 0.000091069145057693, 0.000040318367692028, -0.000012743757610817, -0.000066815793862360, -0.000120497624901022, +-0.000172340560901390, -0.000220842779844105, -0.000264545759556513, -0.000302058112897356, -0.000332100314633963, -0.000353574884310516, +-0.000365597749511169, -0.000367549444935919, -0.000359095079853050, -0.000340248103712097, -0.000311318683345800, -0.000273001982059955, +-0.000226290390127120, -0.000172520816677827, -0.000113294404001585, -0.000050482110010595, 0.000013879538580977, 0.000077596226431135, + 0.000138433205925659, 0.000194162617679696, 0.000242652729081166, 0.000281945254408166, 0.000310336367628720, 0.000326432713773731, + 0.000329234359797551, 0.000318166980387218, 0.000293134593766962, 0.000254529215078380, 0.000203257753881511, 0.000140696086207818, + 0.000068704477919520, -0.000010469460386413, -0.000094210675757463, -0.000179655354658535, -0.000263748377143012, -0.000343385270986054, +-0.000415481708883424, -0.000477127323461832, -0.000525666549407324, -0.000558827052389178, -0.000574804339799704, -0.000572357024711863, +-0.000550859745957779, -0.000510367338359310, -0.000451622691749228, -0.000376069966292593, -0.000285818679043561, -0.000183604586405879, +-0.000072694519055085, 0.000043186973280323, 0.000160008355381864, 0.000273537318290895, 0.000379516624120779, 0.000473794395644265, + 0.000552507414263523, 0.000612205254874264, 0.000650022772194762, 0.000663781194005598, 0.000652114106223388, 0.000614534587819213, + 0.000551495995704878, 0.000464399444796304, 0.000355588960580530, 0.000228287725906860, 0.000086520645722337, -0.000065011393951899, +-0.000221075064189407, -0.000376087180263476, -0.000524295599695647, -0.000660002965562596, -0.000777761692100941, -0.000872600928845689, +-0.000940212655599070, -0.000977149734388767, -0.000980967014779459, -0.000950366012125329, -0.000885265628490655, -0.000786862352853491, +-0.000657621051291283, -0.000501234399294955, -0.000322523208728928, -0.000127303145013843, 0.000077802378562603, 0.000285575230029221, + 0.000488450821113931, 0.000678786843882911, 0.000849143685814374, 0.000992564685207953, 0.001102855954848265, 0.001174835509697094, + 0.001204566114728847, 0.001189538527501501, 0.001128817899509618, 0.001023125083534072, 0.000874874171711282, 0.000688130789013763, + 0.000468524483432417, 0.000223087853969189, -0.000039955342366039, -0.000311455733838924, -0.000581646129972180, -0.000840483383595002, +-0.001078012372713286, -0.001284734478075700, -0.001451980683838214, -0.001572253684600511, -0.001639550394473688, -0.001649630237634784, +-0.001600232998004774, -0.001491225820499409, -0.001324681151893289, -0.001104869603259574, -0.000838175895475649, -0.000532931937919764, +-0.000199170729113807, 0.000151688054986733, 0.000507214917161548, 0.000854400443433744, 0.001180113894521864, 0.001471590125046728, + 0.001716909717985989, 0.001905473631337625, 0.002028431534072068, 0.002079076641927828, 0.002053153126803861, 0.001949105340232015, + 0.001768214443346741, 0.001514653053460190, 0.001195418338212109, 0.000820174992671957, 0.000400977596085337, -0.000048087891083484, +-0.000511363582024283, -0.000972141266167522, -0.001413248730385190, -0.001817663457069480, -0.002169152608635768, -0.002452890782327930, +-0.002656062485219405, -0.002768394343164597, -0.002782628014346268, -0.002694879964376376, -0.002504910400199462, -0.002216242707999800, +-0.001836175428756279, -0.001375629705238660, -0.000848883420232441, -0.000273150191053543, 0.000331940653919483, 0.000944993743897393, + 0.001543560510622334, 0.002104937222707049, 0.002606980311279288, 0.003028946231109706, 0.003352293573213900, 0.003561449536441622, + 0.003644476822562617, 0.003593651107671871, 0.003405886703965055, 0.003083027382420723, 0.002631950003670605, 0.002064510134633659, + 0.001397281869751727, 0.000651143584554952, -0.000149333524704116, -0.000976626519557435, -0.001801160279185895, -0.002592289416964058, +-0.003319380795725522, -0.003952910020607082, -0.004465585199017764, -0.004833410512420773, -0.005036693218115415, -0.005060918229289793, +-0.004897490298049392, -0.004544277445509641, -0.004005965505833022, -0.003294168725949424, -0.002427317265456230, -0.001430285373802798, +-0.000333795576405943, 0.000826424465452056, 0.002010672384642099, 0.003176505000933111, 0.004280095526479507, 0.005277718036143528, + 0.006127269407596632, 0.006789824315509249, 0.007231125920759553, 0.007423007691652700, 0.007344651831210159, 0.006983677069065356, + 0.006336979883000709, 0.005411322883488758, 0.004223614512558328, 0.002800887653706634, 0.001179941307064418, -0.000593330853561824, +-0.002464935213428108, -0.004374055521365783, -0.006254603585906442, -0.008036977828199395, -0.009650011275619909, -0.011023022782754674, +-0.012087943155882672, -0.012781422483705883, -0.013046885087532611, -0.012836436176880656, -0.012112592214956674, -0.010849746583647199, +-0.009035348406797636, -0.006670730134745213, -0.003771569342382457, -0.000367947895237729, 0.003495987348277634, 0.007762773423868341, + 0.012362764963184806, 0.017215633993121138, 0.022232193813321942, 0.027316504310074905, 0.032368195227461656, 0.037284954065171724, + 0.041965097700029354, 0.046310167209203890, 0.050227461840328891, 0.053632450203056022, 0.056450977859688306, 0.058621221189980630, + 0.060095317119274780, 0.060840633157909045, 0.060840633157909045, 0.060095317119274780, 0.058621221189980630, 0.056450977859688306, + 0.053632450203056022, 0.050227461840328891, 0.046310167209203890, 0.041965097700029354, 0.037284954065171724, 0.032368195227461656, + 0.027316504310074905, 0.022232193813321942, 0.017215633993121138, 0.012362764963184806, 0.007762773423868341, 0.003495987348277634, +-0.000367947895237729, -0.003771569342382457, -0.006670730134745213, -0.009035348406797636, -0.010849746583647199, -0.012112592214956674, +-0.012836436176880656, -0.013046885087532611, -0.012781422483705883, -0.012087943155882672, -0.011023022782754674, -0.009650011275619909, +-0.008036977828199395, -0.006254603585906442, -0.004374055521365783, -0.002464935213428108, -0.000593330853561824, 0.001179941307064418, + 0.002800887653706634, 0.004223614512558328, 0.005411322883488758, 0.006336979883000709, 0.006983677069065356, 0.007344651831210159, + 0.007423007691652700, 0.007231125920759553, 0.006789824315509249, 0.006127269407596632, 0.005277718036143528, 0.004280095526479507, + 0.003176505000933111, 0.002010672384642099, 0.000826424465452056, -0.000333795576405943, -0.001430285373802798, -0.002427317265456230, +-0.003294168725949424, -0.004005965505833022, -0.004544277445509641, -0.004897490298049392, -0.005060918229289793, -0.005036693218115415, +-0.004833410512420773, -0.004465585199017764, -0.003952910020607082, -0.003319380795725522, -0.002592289416964058, -0.001801160279185895, +-0.000976626519557435, -0.000149333524704116, 0.000651143584554952, 0.001397281869751727, 0.002064510134633659, 0.002631950003670605, + 0.003083027382420723, 0.003405886703965055, 0.003593651107671871, 0.003644476822562617, 0.003561449536441622, 0.003352293573213900, + 0.003028946231109706, 0.002606980311279288, 0.002104937222707049, 0.001543560510622334, 0.000944993743897393, 0.000331940653919483, +-0.000273150191053543, -0.000848883420232441, -0.001375629705238660, -0.001836175428756279, -0.002216242707999800, -0.002504910400199462, +-0.002694879964376376, -0.002782628014346268, -0.002768394343164597, -0.002656062485219405, -0.002452890782327930, -0.002169152608635768, +-0.001817663457069480, -0.001413248730385190, -0.000972141266167522, -0.000511363582024283, -0.000048087891083484, 0.000400977596085337, + 0.000820174992671957, 0.001195418338212109, 0.001514653053460190, 0.001768214443346741, 0.001949105340232015, 0.002053153126803861, + 0.002079076641927828, 0.002028431534072068, 0.001905473631337625, 0.001716909717985989, 0.001471590125046728, 0.001180113894521864, + 0.000854400443433744, 0.000507214917161548, 0.000151688054986733, -0.000199170729113807, -0.000532931937919764, -0.000838175895475649, +-0.001104869603259574, -0.001324681151893289, -0.001491225820499409, -0.001600232998004774, -0.001649630237634784, -0.001639550394473688, +-0.001572253684600511, -0.001451980683838214, -0.001284734478075700, -0.001078012372713286, -0.000840483383595002, -0.000581646129972180, +-0.000311455733838924, -0.000039955342366039, 0.000223087853969189, 0.000468524483432417, 0.000688130789013763, 0.000874874171711282, + 0.001023125083534072, 0.001128817899509618, 0.001189538527501501, 0.001204566114728847, 0.001174835509697094, 0.001102855954848265, + 0.000992564685207953, 0.000849143685814374, 0.000678786843882911, 0.000488450821113931, 0.000285575230029221, 0.000077802378562603, +-0.000127303145013843, -0.000322523208728928, -0.000501234399294955, -0.000657621051291283, -0.000786862352853491, -0.000885265628490655, +-0.000950366012125329, -0.000980967014779459, -0.000977149734388767, -0.000940212655599070, -0.000872600928845689, -0.000777761692100941, +-0.000660002965562596, -0.000524295599695647, -0.000376087180263476, -0.000221075064189407, -0.000065011393951899, 0.000086520645722337, + 0.000228287725906860, 0.000355588960580530, 0.000464399444796304, 0.000551495995704878, 0.000614534587819213, 0.000652114106223388, + 0.000663781194005598, 0.000650022772194762, 0.000612205254874264, 0.000552507414263523, 0.000473794395644265, 0.000379516624120779, + 0.000273537318290895, 0.000160008355381864, 0.000043186973280323, -0.000072694519055085, -0.000183604586405879, -0.000285818679043561, +-0.000376069966292593, -0.000451622691749228, -0.000510367338359310, -0.000550859745957779, -0.000572357024711863, -0.000574804339799704, +-0.000558827052389178, -0.000525666549407324, -0.000477127323461832, -0.000415481708883424, -0.000343385270986054, -0.000263748377143012, +-0.000179655354658535, -0.000094210675757463, -0.000010469460386413, 0.000068704477919520, 0.000140696086207818, 0.000203257753881511, + 0.000254529215078380, 0.000293134593766962, 0.000318166980387218, 0.000329234359797551, 0.000326432713773731, 0.000310336367628720, + 0.000281945254408166, 0.000242652729081166, 0.000194162617679696, 0.000138433205925659, 0.000077596226431135, 0.000013879538580977, +-0.000050482110010595, -0.000113294404001585, -0.000172520816677827, -0.000226290390127120, -0.000273001982059955, -0.000311318683345800, +-0.000340248103712097, -0.000359095079853050, -0.000367549444935919, -0.000365597749511169, -0.000353574884310516, -0.000332100314633963, +-0.000302058112897356, -0.000264545759556513, -0.000220842779844105, -0.000172340560901390, -0.000120497624901022, -0.000066815793862360, +-0.000012743757610817, 0.000040318367692028, 0.000091069145057693, 0.000138379308460797, 0.000181247345217867, 0.000218894247891487, + 0.000250714121608637, 0.000276344800843569, 0.000295546908455563, 0.000308384566541330, 0.000314971116764541, 0.000315671378571221, + 0.000310952112247671, 0.000301382803316157, 0.000287634273171333, 0.000270439656117594, 0.000250549753299610, 0.000228645026826478, + 0.000205599601545496, 0.000181891356788712, 0.000158328870775841, 0.000135423663054955, 0.000113585644687065, 0.000093266105141811, + 0.000074753467634881, 0.000058212225303250, 0.000043746409775174, 0.000061210247545999 +} ; + +// Sample 300 kHz, pass 20, stop 30, ripple 0. 1dB, atten 100 dB. For 300 to 60 decimation by 5. Pass 0.0667. Stop 0.1000. +double quiskFilt300D5Coefs[125] = { 0.000018530898985935, 0.000048715296542540, 0.000101487713663108, 0.000176187904864114, + 0.000265595698505904, 0.000353014002024914, 0.000412706642676480, 0.000413703988255270, 0.000327138262335125, 0.000135887617633139, +-0.000155548315125899, -0.000514148133587695, -0.000877608176001011, -0.001162065155160332, -0.001278607497803621, -0.001156314863202252, +-0.000766878845626526, -0.000144004619502309, 0.000609514309871448, 0.001332304107659656, 0.001833903451046474, 0.001939453692881414, + 0.001540470537753584, 0.000638747246876731, -0.000630485750733283, -0.002007091925274773, -0.003150529629631723, -0.003715315152624679, +-0.003442798119033104, -0.002247149655474171, -0.000270888264728748, 0.002110922295378516, 0.004348312328871565, 0.005831372793382018, + 0.006041592691834349, 0.004705198784696960, 0.001907765739476663, -0.001866929002333326, -0.005796648417844732, -0.008879322633277557, +-0.010164870856853636, -0.009011364543686401, -0.005302992316722601, 0.000432213665085220, 0.007050324283081295, 0.012980232879613959, + 0.016550056909769441, 0.016390975010318803, 0.011831378367146397, 0.003185612579985462, -0.008144878931421762, -0.019805426351182119, +-0.028854993458539563, -0.032310238090111396, -0.027765474190972032, -0.013961139511732867, 0.008825649334690966, 0.038665457076335182, + 0.072179830176428442, 0.105042015460381060, 0.132689511350380370, 0.151114898773723890, 0.157580579198349920, 0.151114898773723890, + 0.132689511350380370, 0.105042015460381060, 0.072179830176428442, 0.038665457076335182, 0.008825649334690966, -0.013961139511732867, +-0.027765474190972032, -0.032310238090111396, -0.028854993458539563, -0.019805426351182119, -0.008144878931421762, 0.003185612579985462, + 0.011831378367146397, 0.016390975010318803, 0.016550056909769441, 0.012980232879613959, 0.007050324283081295, 0.000432213665085220, +-0.005302992316722601, -0.009011364543686401, -0.010164870856853636, -0.008879322633277557, -0.005796648417844732, -0.001866929002333326, + 0.001907765739476663, 0.004705198784696960, 0.006041592691834349, 0.005831372793382018, 0.004348312328871565, 0.002110922295378516, +-0.000270888264728748, -0.002247149655474171, -0.003442798119033104, -0.003715315152624679, -0.003150529629631723, -0.002007091925274773, +-0.000630485750733283, 0.000638747246876731, 0.001540470537753584, 0.001939453692881414, 0.001833903451046474, 0.001332304107659656, + 0.000609514309871448, -0.000144004619502309, -0.000766878845626526, -0.001156314863202252, -0.001278607497803621, -0.001162065155160332, +-0.000877608176001011, -0.000514148133587695, -0.000155548315125899, 0.000135887617633139, 0.000327138262335125, 0.000413703988255270, + 0.000412706642676480, 0.000353014002024914, 0.000265595698505904, 0.000176187904864114, 0.000101487713663108, 0.000048715296542540, + 0.000018530898985935 +} ; + +// Sample 240 kHz, pass 20, stop 30, ripple 0. 1dB, atten 100 dB. For 240 to 60 decimation by 4. Pass 0.0833. Stop 0.125. +double quiskFilt240D4Coefs[100] = { 0.000026427748038218, 0.000083044892122331, 0.000183911274066307, 0.000320349110829452, + 0.000457388985778469, 0.000532620344169036, 0.000470872819082771, 0.000214313769983376, -0.000240081570728570, -0.000811566364292325, +-0.001333777016038777, -0.001592688419586267, -0.001402284132170216, -0.000696434758718897, 0.000400157800248161, 0.001562276075007405, + 0.002340457068600146, 0.002316140009670930, 0.001289626552449154, -0.000573307985418704, -0.002713127282322421, -0.004311143720675439, +-0.004570909733165466, -0.003061204879955289, 0.000013040647807287, 0.003746101513420969, 0.006749294361298744, 0.007623049432174923, + 0.005539556386039066, 0.000714048117939475, -0.005457094353399323, -0.010719487844933161, -0.012706806783966153, -0.009888987455409530, +-0.002375961043231839, 0.007777042412925690, 0.016982700467123999, 0.021228739037755177, 0.017579411755468045, 0.005569962317355060, +-0.012047196260398848, -0.029586753168515320, -0.039804931920814045, -0.036039635084008419, -0.014481769698543458, 0.024172310628567603, + 0.074299385648554650, 0.126409178605880910, 0.169462317675936840, 0.193798358783223160, 0.193798358783223160, 0.169462317675936840, + 0.126409178605880910, 0.074299385648554650, 0.024172310628567603, -0.014481769698543458, -0.036039635084008419, -0.039804931920814045, +-0.029586753168515320, -0.012047196260398848, 0.005569962317355060, 0.017579411755468045, 0.021228739037755177, 0.016982700467123999, + 0.007777042412925690, -0.002375961043231839, -0.009888987455409530, -0.012706806783966153, -0.010719487844933161, -0.005457094353399323, + 0.000714048117939475, 0.005539556386039066, 0.007623049432174923, 0.006749294361298744, 0.003746101513420969, 0.000013040647807287, +-0.003061204879955289, -0.004570909733165466, -0.004311143720675439, -0.002713127282322421, -0.000573307985418704, 0.001289626552449154, + 0.002316140009670930, 0.002340457068600146, 0.001562276075007405, 0.000400157800248161, -0.000696434758718897, -0.001402284132170216, +-0.001592688419586267, -0.001333777016038777, -0.000811566364292325, -0.000240081570728570, 0.000214313769983376, 0.000470872819082771, + 0.000532620344169036, 0.000457388985778469, 0.000320349110829452, 0.000183911274066307, 0.000083044892122331, 0.000026427748038218 +} ; + +// Sample 300 kHz, pass 20, stop 25, ripple 0. 1dB, atten 100 dB. For 300 to 50 decimation by 6. Pass 0.0667. Stop 0.08333. +double quiskFilt300D6Coefs[248] = { + 0.000011910887251416, 0.000022105099505161, 0.000038528164880090, 0.000058057974756687, 0.000077529348136962, 0.000092122417522252, + 0.000095773826807872, 0.000082150606579247, 0.000045941802546827, -0.000015655964463738, -0.000101508560175109, -0.000205623901768475, +-0.000317020911117816, -0.000420598298959303, -0.000499105467381654, -0.000536009730130428, -0.000518834322912359, -0.000442349591905865, +-0.000310909623154438, -0.000139289407787980, 0.000048450839869391, 0.000222182374045520, 0.000350656872582151, 0.000407443247027791, + 0.000376767722059760, 0.000258013352391244, 0.000067733888072331, -0.000161597279767678, -0.000386276495667759, -0.000558969004494543, +-0.000638008043834271, -0.000596639687427168, -0.000430244947175777, -0.000159760966769872, 0.000169879976202206, 0.000496544732562163, + 0.000752090455258835, 0.000876070391837635, 0.000829307042381563, 0.000604420257362057, 0.000230722859227965, -0.000228175424415781, +-0.000684317047397475, -0.001041562295798296, -0.001215154241382078, -0.001150839763823582, -0.000839401588636812, -0.000323015778972820, + 0.000308742853689313, 0.000933405036813186, 0.001418532658612045, 0.001648958563352753, 0.001552907570724076, 0.001121286921437812, + 0.000415365221846381, -0.000439881924253269, -0.001276648649153385, -0.001916215180556943, -0.002206052212475925, -0.002054620852694224, +-0.001456203929560064, -0.000499569542945491, 0.000642800486076826, 0.001744295867778604, 0.002567858832132124, 0.002915611567235574, + 0.002674313538272378, 0.001846569533541176, 0.000559918306471771, -0.000949983449361535, -0.002380635105445952, -0.003421623936658742, +-0.003820082151371851, -0.003439232264380022, -0.002296966067370011, -0.000574611485798243, 0.001408391062695743, 0.003251259943133494, + 0.004550539113003901, 0.004985688082005479, 0.004393958051961603, 0.002817791913037669, 0.000512488271545885, -0.002090680856085413, +-0.004461658971355160, -0.006076001180326797, -0.006526596735626234, -0.005619716454301121, -0.003433859382364719, -0.000325983426037548, + 0.003120481512082456, 0.006199690635806164, 0.008221988623961972, 0.008660926799975453, 0.007278698204181967, 0.004202043225935329, +-0.000071349813179933, -0.004743225902744831, -0.008854023235662239, -0.011468359077217744, -0.011872788367371184, -0.009746561766167391, +-0.005268271950313840, 0.000868984112618413, 0.007545384655588967, 0.013397186502100830, 0.017067367815119631, 0.017481531706887495, + 0.014098209764418178, 0.007083578133321482, -0.002628980063776467, -0.013415472624288912, -0.023171404153239539, -0.029628134880484221, +-0.030727216816581020, -0.024991056056960267, -0.011825325274110144, 0.008302942014898000, 0.033848791339949939, 0.062345366630597851, + 0.090714512362454230, 0.115687137518083420, 0.134268728793426880, 0.144176283697368230, 0.144176283697368230, 0.134268728793426880, + 0.115687137518083420, 0.090714512362454230, 0.062345366630597851, 0.033848791339949939, 0.008302942014898000, -0.011825325274110144, +-0.024991056056960267, -0.030727216816581020, -0.029628134880484221, -0.023171404153239539, -0.013415472624288912, -0.002628980063776467, + 0.007083578133321482, 0.014098209764418178, 0.017481531706887495, 0.017067367815119631, 0.013397186502100830, 0.007545384655588967, + 0.000868984112618413, -0.005268271950313840, -0.009746561766167391, -0.011872788367371184, -0.011468359077217744, -0.008854023235662239, +-0.004743225902744831, -0.000071349813179933, 0.004202043225935329, 0.007278698204181967, 0.008660926799975453, 0.008221988623961972, + 0.006199690635806164, 0.003120481512082456, -0.000325983426037548, -0.003433859382364719, -0.005619716454301121, -0.006526596735626234, +-0.006076001180326797, -0.004461658971355160, -0.002090680856085413, 0.000512488271545885, 0.002817791913037669, 0.004393958051961603, + 0.004985688082005479, 0.004550539113003901, 0.003251259943133494, 0.001408391062695743, -0.000574611485798243, -0.002296966067370011, +-0.003439232264380022, -0.003820082151371851, -0.003421623936658742, -0.002380635105445952, -0.000949983449361535, 0.000559918306471771, + 0.001846569533541176, 0.002674313538272378, 0.002915611567235574, 0.002567858832132124, 0.001744295867778604, 0.000642800486076826, +-0.000499569542945491, -0.001456203929560064, -0.002054620852694224, -0.002206052212475925, -0.001916215180556943, -0.001276648649153385, +-0.000439881924253269, 0.000415365221846381, 0.001121286921437812, 0.001552907570724076, 0.001648958563352753, 0.001418532658612045, + 0.000933405036813186, 0.000308742853689313, -0.000323015778972820, -0.000839401588636812, -0.001150839763823582, -0.001215154241382078, +-0.001041562295798296, -0.000684317047397475, -0.000228175424415781, 0.000230722859227965, 0.000604420257362057, 0.000829307042381563, + 0.000876070391837635, 0.000752090455258835, 0.000496544732562163, 0.000169879976202206, -0.000159760966769872, -0.000430244947175777, +-0.000596639687427168, -0.000638008043834271, -0.000558969004494543, -0.000386276495667759, -0.000161597279767678, 0.000067733888072331, + 0.000258013352391244, 0.000376767722059760, 0.000407443247027791, 0.000350656872582151, 0.000222182374045520, 0.000048450839869391, +-0.000139289407787980, -0.000310909623154438, -0.000442349591905865, -0.000518834322912359, -0.000536009730130428, -0.000499105467381654, +-0.000420598298959303, -0.000317020911117816, -0.000205623901768475, -0.000101508560175109, -0.000015655964463738, 0.000045941802546827, + 0.000082150606579247, 0.000095773826807872, 0.000092122417522252, 0.000077529348136962, 0.000058057974756687, 0.000038528164880090, + 0.000022105099505161, 0.000011910887251416 +} ; + +// Sample 48 kHz, differentiator, pass 21000 ripple 5db, stop 23999 to 24000 atten 20 dB +double quiskDiff48Coefs[38] = { -0.000346604486329388, 0.000872060526788775, -0.001377888428055412, 0.002143851727666205, +-0.003140241321402629, 0.004374640214997344, -0.005845903317976450, 0.007539093249300473, -0.009422827615507629, 0.011447847797822517, +-0.013545904360670370, 0.015627972401100334, -0.017577926784897915, 0.019235081888911390, -0.020342464109632617, + 0.020366421977821021, -0.017707206191343410, 0.004204607038114273, 0.175457827152511820, -0.175457827152511820, +-0.004204607038114273, 0.017707206191343410, -0.020366421977821021, 0.020342464109632617, -0.019235081888911390, + 0.017577926784897915, -0.015627972401100334, 0.013545904360670370, -0.011447847797822517, 0.009422827615507629, +-0.007539093249300473, 0.005845903317976450, -0.004374640214997344, 0.003140241321402629, -0.002143851727666205, + 0.001377888428055412, -0.000872060526788775, 0.000346604486329388 +} ; diff --git a/filters.py b/filters.py new file mode 100644 index 0000000..bcf066c --- /dev/null +++ b/filters.py @@ -0,0 +1,770 @@ +# Rate 24000 sps, ripple 0.2 dB, atten 100 dB, shape 1.2 +# Filters key is the bandwidth at 24000 sps. +# key = bw * 24000 / rate / 2 +# bw = key * 2 * rate / 24000 +Filters = { + +8500: # 55 taps +[ -0.001578241201420500, -0.002488309592652162, 0.001185909979889287, 0.000286041612735020, -0.001914552184024928, 0.002760744242385052, +-0.001796589368523962, -0.001056335106849282, 0.004390770806455585, -0.005824325923037834, 0.003465568616258560, 0.002462568821267293, +-0.008988281812658983, 0.011629434198087422, -0.007027876977623796, -0.004272818661253936, 0.016890540812408691, -0.022619963224181760, + 0.014949848668575091, 0.006115840936585570, -0.031813780331403710, 0.046915611796058194, -0.036226209362519565, -0.007500120805011563, + 0.078216492123247466, -0.156996838729026640, 0.218668091485032380, 0.758014834761081340, 0.218668091485032380, -0.156996838729026640, + 0.078216492123247466, -0.007500120805011563, -0.036226209362519565, 0.046915611796058194, -0.031813780331403710, 0.006115840936585570, + 0.014949848668575091, -0.022619963224181760, 0.016890540812408691, -0.004272818661253936, -0.007027876977623796, 0.011629434198087422, +-0.008988281812658983, 0.002462568821267293, 0.003465568616258560, -0.005824325923037834, 0.004390770806455585, -0.001056335106849282, +-0.001796589368523962, 0.002760744242385052, -0.001914552184024928, 0.000286041612735020, 0.001185909979889287, -0.002488309592652162, +-0.001578241201420500], + +7500: # 62 taps +[ 0.000094561136776712, 0.001408783281963872, 0.003252521294198745, 0.001088470223697677, -0.002238006126669353, 0.000797427337925380, + 0.001818354390057951, -0.002971083246008131, 0.000737271087741229, 0.003304967641132078, -0.004678221609224611, 0.000618143813431047, + 0.005732278632558854, -0.007264716193352521, 0.000382151456262394, 0.009375552017313197, -0.011107789354244654, 0.000048810931888440, + 0.014797494013431127, -0.016988015917755381, -0.000322746345617388, 0.023289857616213484, -0.026716501910571362, -0.000681860486120778, + 0.038508535988246995, -0.045999278172093885, -0.000961050429347091, 0.076884527279961243, -0.108006033576806130, -0.001113929105132611, + 0.551548401015432340, 0.551548401015432340, -0.001113929105132611, -0.108006033576806130, 0.076884527279961243, -0.000961050429347091, +-0.045999278172093885, 0.038508535988246995, -0.000681860486120778, -0.026716501910571362, 0.023289857616213484, -0.000322746345617388, +-0.016988015917755381, 0.014797494013431127, 0.000048810931888440, -0.011107789354244654, 0.009375552017313197, 0.000382151456262394, +-0.007264716193352521, 0.005732278632558854, 0.000618143813431047, -0.004678221609224611, 0.003304967641132078, 0.000737271087741229, +-0.002971083246008131, 0.001818354390057951, 0.000797427337925380, -0.002238006126669353, 0.001088470223697677, 0.003252521294198745, + 0.001408783281963872, 0.000094561136776712], + +6000: # 77 taps + [0.000208183216794497, 0.001160220144497454, 0.002498877019501132, 0.002217684703504632, -0.000364985361618348, -0.001823661861823501, + 0.000356579314286080, 0.002005946277037424, -0.000685757071824683, -0.002428321330453817, 0.001256816376812629, 0.002943833745660115, +-0.002130949481744204, -0.003446793644558851, 0.003380457505759830, 0.003807055791440830, -0.005074958590792311, -0.003869772827804518, + 0.007242784565398008, 0.003418656761533474, -0.009894698971107840, -0.002211451952907241, 0.012977467451293289, -0.000058006701182926, +-0.016390443243429843, 0.003773319671211690, 0.019989716564592076, -0.009449626980637388, -0.023581188491464997, 0.017927337095200193, + 0.026945533578336647, -0.030887338584165511, -0.029856365801280219, 0.052657593695444822, 0.032108551641510746, -0.099253908185103573, +-0.033533139787951992, 0.315983886115891840, 0.534019780032233380, 0.315983886115891840, -0.033533139787951992, -0.099253908185103573, + 0.032108551641510746, 0.052657593695444822, -0.029856365801280219, -0.030887338584165511, 0.026945533578336647, 0.017927337095200193, +-0.023581188491464997, -0.009449626980637388, 0.019989716564592076, 0.003773319671211690, -0.016390443243429843, -0.000058006701182926, + 0.012977467451293289, -0.002211451952907241, -0.009894698971107840, 0.003418656761533474, 0.007242784565398008, -0.003869772827804518, +-0.005074958590792311, 0.003807055791440830, 0.003380457505759830, -0.003446793644558851, -0.002130949481744204, 0.002943833745660115, + 0.001256816376812629, -0.002428321330453817, -0.000685757071824683, 0.002005946277037424, 0.000356579314286080, -0.001823661861823501, +-0.000364985361618348, 0.002217684703504632, 0.002498877019501132, 0.001160220144497454, 0.000208183216794497], + +5000: # 92 taps +[ 0.000172282027074915, 0.000805722137099302, 0.001789488027434626, 0.002208898476835740, 0.001133555920432830, -0.000798012231727563, +-0.001468876483570138, 0.000053213574826391, 0.001640044205108085, 0.000693959429133929, -0.001663633271521857, -0.001609719846438075, + 0.001334915862232435, 0.002601113060240987, -0.000511327252542249, -0.003459777069001452, -0.000863807590293483, 0.003915575549171324, + 0.002725579328876624, -0.003678783180717698, -0.004875619345633069, 0.002500119680365451, 0.006978228022424931, -0.000223624762857588, +-0.008586543420189311, -0.003156720729060025, 0.009180028261871051, 0.007460886285077675, -0.008222908809263846, -0.012298402032805727, + 0.005219135899689294, 0.017069401572755501, 0.000240426913514277, -0.020971624610692834, -0.008474182212803629, 0.022996901553415793, + 0.019787130769856574, -0.021836278313112276, -0.034759648853466663, 0.015468734639474426, 0.055194479055695952, 0.000590985958335975, +-0.088238504731419823, -0.043831925654670714, 0.182996493758220890, 0.409587845444253250, 0.409587845444253250, 0.182996493758220890, +-0.043831925654670714, -0.088238504731419823, 0.000590985958335975, 0.055194479055695952, 0.015468734639474426, -0.034759648853466663, +-0.021836278313112276, 0.019787130769856574, 0.022996901553415793, -0.008474182212803629, -0.020971624610692834, 0.000240426913514277, + 0.017069401572755501, 0.005219135899689294, -0.012298402032805727, -0.008222908809263846, 0.007460886285077675, 0.009180028261871051, +-0.003156720729060025, -0.008586543420189311, -0.000223624762857588, 0.006978228022424931, 0.002500119680365451, -0.004875619345633069, +-0.003678783180717698, 0.002725579328876624, 0.003915575549171324, -0.000863807590293483, -0.003459777069001452, -0.000511327252542249, + 0.002601113060240987, 0.001334915862232435, -0.001609719846438075, -0.001663633271521857, 0.000693959429133929, 0.001640044205108085, + 0.000053213574826391, -0.001468876483570138, -0.000798012231727563, 0.001133555920432830, 0.002208898476835740, 0.001789488027434626, + 0.000805722137099302, 0.000172282027074915], + +4500: # 102 taps +[ 0.000109626786353929, 0.000462685347095325, 0.000976091511950423, 0.001146819725082014, 0.000393473830731237, -0.001141511548465342, +-0.002282834370334400, -0.001829207175666342, -0.000101528442900756, 0.000987382913028173, 0.000039751448950354, -0.001822373509053446, +-0.001985247021210946, 0.000232895236822806, 0.002153431919758378, 0.000930723811183338, -0.002248491432658168, -0.003017055190706798, + 0.000342078561845510, 0.003721285661215643, 0.002085819103613060, -0.003140571194897081, -0.004835117960215140, 0.000288263563014682, + 0.005924235485555640, 0.003715592386573005, -0.004558690834977428, -0.007633320493226938, 0.000139066056400393, 0.009165154333957292, + 0.006093793617689697, -0.006780247949818048, -0.011926916343123978, -0.000057228900700871, 0.014232549280021243, 0.009771685994328400, +-0.010499186884061604, -0.019033587881754202, -0.000249830231388923, 0.023228171670193941, 0.016360728644062135, -0.017855007053639658, +-0.033458833271228797, -0.000405885453398987, 0.044679917304132288, 0.033455780397647107, -0.040439521683945157, -0.085684078775375178, +-0.000493182012170114, 0.201251835014007290, 0.374516101600616320, 0.374516101600616320, 0.201251835014007290, -0.000493182012170114, +-0.085684078775375178, -0.040439521683945157, 0.033455780397647107, 0.044679917304132288, -0.000405885453398987, -0.033458833271228797, +-0.017855007053639658, 0.016360728644062135, 0.023228171670193941, -0.000249830231388923, -0.019033587881754202, -0.010499186884061604, + 0.009771685994328400, 0.014232549280021243, -0.000057228900700871, -0.011926916343123978, -0.006780247949818048, 0.006093793617689697, + 0.009165154333957292, 0.000139066056400393, -0.007633320493226938, -0.004558690834977428, 0.003715592386573005, 0.005924235485555640, + 0.000288263563014682, -0.004835117960215140, -0.003140571194897081, 0.002085819103613060, 0.003721285661215643, 0.000342078561845510, +-0.003017055190706798, -0.002248491432658168, 0.000930723811183338, 0.002153431919758378, 0.000232895236822806, -0.001985247021210946, +-0.001822373509053446, 0.000039751448950354, 0.000987382913028173, -0.000101528442900756, -0.001829207175666342, -0.002282834370334400, +-0.001141511548465342, 0.000393473830731237, 0.001146819725082014, 0.000976091511950423, 0.000462685347095325, 0.000109626786353929], + +4000: # 115 taps +[ 0.000078524780320715, 0.000329316620713670, 0.000756688762644958, 0.001124260123708105, 0.001012255791778806, 0.000167097630541898, +-0.001099970608704988, -0.001940975178324101, -0.001625045960154994, -0.000333634994172134, 0.000774600995458535, 0.000561151578352762, +-0.000854751315214004, -0.001984988321940357, -0.001407739886101419, 0.000563473972774177, 0.001900424989770745, 0.000925528523083090, +-0.001609328373038515, -0.002935359144493341, -0.001199176692593675, 0.002118199425756780, 0.003340065554812114, 0.000621160360920014, +-0.003467567745617676, -0.004224605385298791, -0.000122849763708023, 0.004757557369118008, 0.004664298923525774, -0.001148493552322016, +-0.006629807337962404, -0.005080846185003255, 0.002831233307698604, 0.008609924128977818, 0.004891354675929344, -0.005425659232916345, +-0.010937452229276256, -0.004116901228197529, 0.008877312794209294, 0.013300805492116527, 0.002232134062032589, -0.013614806007146969, +-0.015720726837983701, 0.001140172406302503, 0.019985979932262889, 0.017946437617680795, -0.007018595519087580, -0.029113241143045584, +-0.019901010720465118, 0.017502959326722210, 0.043879416112787749, 0.021395356319346116, -0.039872421015430898, -0.076438935865810770, +-0.022353637195009195, 0.124776719393937450, 0.286085089477746360, 0.356006518780215160, 0.286085089477746360, 0.124776719393937450, +-0.022353637195009195, -0.076438935865810770, -0.039872421015430898, 0.021395356319346116, 0.043879416112787749, 0.017502959326722210, +-0.019901010720465118, -0.029113241143045584, -0.007018595519087580, 0.017946437617680795, 0.019985979932262889, 0.001140172406302503, +-0.015720726837983701, -0.013614806007146969, 0.002232134062032589, 0.013300805492116527, 0.008877312794209294, -0.004116901228197529, +-0.010937452229276256, -0.005425659232916345, 0.004891354675929344, 0.008609924128977818, 0.002831233307698604, -0.005080846185003255, +-0.006629807337962404, -0.001148493552322016, 0.004664298923525774, 0.004757557369118008, -0.000122849763708023, -0.004224605385298791, +-0.003467567745617676, 0.000621160360920014, 0.003340065554812114, 0.002118199425756780, -0.001199176692593675, -0.002935359144493341, +-0.001609328373038515, 0.000925528523083090, 0.001900424989770745, 0.000563473972774177, -0.001407739886101419, -0.001984988321940357, +-0.000854751315214004, 0.000561151578352762, 0.000774600995458535, -0.000333634994172134, -0.001625045960154994, -0.001940975178324101, +-0.001099970608704988, 0.000167097630541898, 0.001012255791778806, 0.001124260123708105, 0.000756688762644958, 0.000329316620713670, + 0.000078524780320715], + +3000: # 153 taps +[ 0.000035516842253053, 0.000120241356692793, 0.000262655558849812, 0.000421822903927101, 0.000504577556110132, 0.000393748344899348, + 0.000013887896365045, -0.000595749610102644, -0.001250516069080062, -0.001672797229233531, -0.001624666112532619, -0.001059802835950466, +-0.000199611602796516, 0.000539126532493226, 0.000756552265297489, 0.000316087930054194, -0.000530090257617794, -0.001244496438584232, +-0.001312107606392913, -0.000583465448463906, 0.000572285419075017, 0.001440987738923421, 0.001399231913182619, 0.000355359654986622, +-0.001108071194955440, -0.002036249585608732, -0.001707924634389252, -0.000169594756520662, 0.001685676765584473, 0.002618141236597755, + 0.001855681542593165, -0.000301881949096732, -0.002542398236828585, -0.003317624887398237, -0.001892391320644168, 0.001031582912020721, + 0.003613502326574886, 0.004009329165673526, 0.001661239094822322, -0.002143832449766766, -0.004935146076383144, -0.004634572876828250, +-0.001061541395886805, 0.003698344992855291, 0.006465358367957256, 0.005053382115343493, -0.000073525601278193, -0.005791007488487868, +-0.008166064459464053, -0.005109407447857288, 0.001926336767092539, 0.008511258651897639, 0.009958456933858797, 0.004574761558815073, +-0.004759500034603159, -0.012000283350062089, -0.011748002733961208, -0.003120526928071337, 0.008993494047071084, 0.016529385910476514, + 0.013426861274101313, 0.000181178754148492, -0.015466003330041472, -0.022748666494653846, -0.014877802849879123, 0.005502737892245722, + 0.026345716718994539, 0.032621748472455206, 0.016002483863926359, -0.017887767208878101, -0.049636964594866217, -0.054713319818683277, +-0.016713474537575129, 0.061906959876918494, 0.157995584096340960, 0.236648015289099320, 0.266956938993864910, 0.236648015289099320, + 0.157995584096340960, 0.061906959876918494, -0.016713474537575129, -0.054713319818683277, -0.049636964594866217, -0.017887767208878101, + 0.016002483863926359, 0.032621748472455206, 0.026345716718994539, 0.005502737892245722, -0.014877802849879123, -0.022748666494653846, +-0.015466003330041472, 0.000181178754148492, 0.013426861274101313, 0.016529385910476514, 0.008993494047071084, -0.003120526928071337, +-0.011748002733961208, -0.012000283350062089, -0.004759500034603159, 0.004574761558815073, 0.009958456933858797, 0.008511258651897639, + 0.001926336767092539, -0.005109407447857288, -0.008166064459464053, -0.005791007488487868, -0.000073525601278193, 0.005053382115343493, + 0.006465358367957256, 0.003698344992855291, -0.001061541395886805, -0.004634572876828250, -0.004935146076383144, -0.002143832449766766, + 0.001661239094822322, 0.004009329165673526, 0.003613502326574886, 0.001031582912020721, -0.001892391320644168, -0.003317624887398237, +-0.002542398236828585, -0.000301881949096732, 0.001855681542593165, 0.002618141236597755, 0.001685676765584473, -0.000169594756520662, +-0.001707924634389252, -0.002036249585608732, -0.001108071194955440, 0.000355359654986622, 0.001399231913182619, 0.001440987738923421, + 0.000572285419075017, -0.000583465448463906, -0.001312107606392913, -0.001244496438584232, -0.000530090257617794, 0.000316087930054194, + 0.000756552265297489, 0.000539126532493226, -0.000199611602796516, -0.001059802835950466, -0.001624666112532619, -0.001672797229233531, +-0.001250516069080062, -0.000595749610102644, 0.000013887896365045, 0.000393748344899348, 0.000504577556110132, 0.000421822903927101, + 0.000262655558849812, 0.000120241356692793, 0.000035516842253053], + +2800: # 164 taps +[ 0.000031131729843669, 0.000101363194753254, 0.000221445426297415, 0.000366876574918002, 0.000474767078312489, 0.000455842035531617, + 0.000232260566090893, -0.000211274148558287, -0.000789599231892209, -0.001321558408924163, -0.001590694231360794, -0.001445876584766002, +-0.000895790759735873, -0.000140741015448872, 0.000492974178868593, 0.000701994832615354, 0.000371048524479058, -0.000337331657814867, +-0.001034893398244673, -0.001296923457098872, -0.000901505495786621, 0.000013737161230886, 0.000983722390445291, 0.001446949255083495, + 0.001067648443145340, -0.000031354774249279, -0.001279007610852184, -0.001938862367504847, -0.001531888866790246, -0.000161657161535566, + 0.001470243306104189, 0.002410991575715065, 0.002003292184824320, 0.000324608046693798, -0.001764616004515830, -0.003045092340783782, +-0.002637146414877572, -0.000580772176139985, 0.002073712223135724, 0.003782183003317882, 0.003389875946583850, 0.000885046737439423, +-0.002455184150014141, -0.004690784048082864, -0.004326357662532086, -0.001279167957787954, 0.002902671922490944, 0.005795008254325585, + 0.005478283962467929, 0.001772012477148658, -0.003450331904449436, -0.007166940301855534, -0.006922766471023288, -0.002399780547747377, + 0.004134972375816635, 0.008909643621809859, 0.008776960071400969, 0.003217389082858829, -0.005021094247026112, -0.011201040385085154, +-0.011246577131734643, -0.004323921802028225, 0.006225285499714047, 0.014372818914766290, 0.014725605621083649, 0.005913959753070873, +-0.007987043798634774, -0.019127577707865114, -0.020080950001885520, -0.008435384367710340, 0.010879472951485192, 0.027244661531034900, + 0.029649597250285761, 0.013188790214990415, -0.016693663421477496, -0.044885495280403948, -0.052572421004629472, -0.026098663081897257, + 0.035426680640922303, 0.117691081437968410, 0.195575709828410660, 0.242828423889182740, 0.242828423889182740, 0.195575709828410660, + 0.117691081437968410, 0.035426680640922303, -0.026098663081897257, -0.052572421004629472, -0.044885495280403948, -0.016693663421477496, + 0.013188790214990415, 0.029649597250285761, 0.027244661531034900, 0.010879472951485192, -0.008435384367710340, -0.020080950001885520, +-0.019127577707865114, -0.007987043798634774, 0.005913959753070873, 0.014725605621083649, 0.014372818914766290, 0.006225285499714047, +-0.004323921802028225, -0.011246577131734643, -0.011201040385085154, -0.005021094247026112, 0.003217389082858829, 0.008776960071400969, + 0.008909643621809859, 0.004134972375816635, -0.002399780547747377, -0.006922766471023288, -0.007166940301855534, -0.003450331904449436, + 0.001772012477148658, 0.005478283962467929, 0.005795008254325585, 0.002902671922490944, -0.001279167957787954, -0.004326357662532086, +-0.004690784048082864, -0.002455184150014141, 0.000885046737439423, 0.003389875946583850, 0.003782183003317882, 0.002073712223135724, +-0.000580772176139985, -0.002637146414877572, -0.003045092340783782, -0.001764616004515830, 0.000324608046693798, 0.002003292184824320, + 0.002410991575715065, 0.001470243306104189, -0.000161657161535566, -0.001531888866790246, -0.001938862367504847, -0.001279007610852184, +-0.000031354774249279, 0.001067648443145340, 0.001446949255083495, 0.000983722390445291, 0.000013737161230886, -0.000901505495786621, +-0.001296923457098872, -0.001034893398244673, -0.000337331657814867, 0.000371048524479058, 0.000701994832615354, 0.000492974178868593, +-0.000140741015448872, -0.000895790759735873, -0.001445876584766002, -0.001590694231360794, -0.001321558408924163, -0.000789599231892209, +-0.000211274148558287, 0.000232260566090893, 0.000455842035531617, 0.000474767078312489, 0.000366876574918002, 0.000221445426297415, + 0.000101363194753254, 0.000031131729843669], + +2500: # 184 taps +[ 0.000025917992771962, 0.000080053245121681, 0.000175970492512707, 0.000306338738553062, 0.000441565642149420, 0.000529323030925432, + 0.000506924615738917, 0.000324946604356638, -0.000025129491557388, -0.000491406291157732, -0.000962621820235116, -0.001296070805120129, +-0.001367486032675225, -0.001125819560345427, -0.000629044038382859, -0.000039974702137716, 0.000424508165741649, 0.000577252836600570, + 0.000345217289298406, -0.000184075454087644, -0.000781983763019378, -0.001165504636264451, -0.001120965614204760, -0.000611972955861357, + 0.000181852749867249, 0.000923465749187075, 0.001259585458205595, 0.000987840637523650, 0.000172679353454093, -0.000852839379782549, +-0.001614970097646428, -0.001711408440970664, -0.001014931729801465, 0.000231871248756494, 0.001490613857222986, 0.002154309898448981, + 0.001835132214640792, 0.000579040055238457, -0.001106680482118841, -0.002448432281636389, -0.002754083239929045, -0.001760402090267228, + 0.000190461179667856, 0.002251447413131553, 0.003429705849477585, 0.003056701038439272, 0.001150595364468159, -0.001518817798865299, +-0.003726827835919960, -0.004345414044232734, -0.002906143603126905, 0.000100825920300210, 0.003366376342127399, 0.005318377282596896, + 0.004864845763651887, 0.001979300066273931, -0.002175246374000726, -0.005692516278767321, -0.006780045002879058, -0.004649909783330805, +-0.000010396809644141, 0.005123662380431661, 0.008280363892442090, 0.007693898397635463, 0.003246147289573304, -0.003296547115364492, +-0.008942285826077026, -0.010803588267982528, -0.007523983582706444, -0.000118131836810526, 0.008247900700950585, 0.013549640285344118, + 0.012768247956349992, 0.005492469173933735, -0.005544176969275239, -0.015378525898323159, -0.018928522076093565, -0.013447400254072768, +-0.000204681888673014, 0.015499492179231059, 0.026221846254622386, 0.025562428680330915, 0.011389219733612054, -0.012327402868838598, +-0.036056520568517325, -0.047698955925824099, -0.037076675237560336, -0.000252599132841553, 0.058062325226935912, 0.125125149137395140, + 0.183789926776792010, 0.217965757483672360, 0.217965757483672360, 0.183789926776792010, 0.125125149137395140, 0.058062325226935912, +-0.000252599132841553, -0.037076675237560336, -0.047698955925824099, -0.036056520568517325, -0.012327402868838598, 0.011389219733612054, + 0.025562428680330915, 0.026221846254622386, 0.015499492179231059, -0.000204681888673014, -0.013447400254072768, -0.018928522076093565, +-0.015378525898323159, -0.005544176969275239, 0.005492469173933735, 0.012768247956349992, 0.013549640285344118, 0.008247900700950585, +-0.000118131836810526, -0.007523983582706444, -0.010803588267982528, -0.008942285826077026, -0.003296547115364492, 0.003246147289573304, + 0.007693898397635463, 0.008280363892442090, 0.005123662380431661, -0.000010396809644141, -0.004649909783330805, -0.006780045002879058, +-0.005692516278767321, -0.002175246374000726, 0.001979300066273931, 0.004864845763651887, 0.005318377282596896, 0.003366376342127399, + 0.000100825920300210, -0.002906143603126905, -0.004345414044232734, -0.003726827835919960, -0.001518817798865299, 0.001150595364468159, + 0.003056701038439272, 0.003429705849477585, 0.002251447413131553, 0.000190461179667856, -0.001760402090267228, -0.002754083239929045, +-0.002448432281636389, -0.001106680482118841, 0.000579040055238457, 0.001835132214640792, 0.002154309898448981, 0.001490613857222986, + 0.000231871248756494, -0.001014931729801465, -0.001711408440970664, -0.001614970097646428, -0.000852839379782549, 0.000172679353454093, + 0.000987840637523650, 0.001259585458205595, 0.000923465749187075, 0.000181852749867249, -0.000611972955861357, -0.001120965614204760, +-0.001165504636264451, -0.000781983763019378, -0.000184075454087644, 0.000345217289298406, 0.000577252836600570, 0.000424508165741649, +-0.000039974702137716, -0.000629044038382859, -0.001125819560345427, -0.001367486032675225, -0.001296070805120129, -0.000962621820235116, +-0.000491406291157732, -0.000025129491557388, 0.000324946604356638, 0.000506924615738917, 0.000529323030925432, 0.000441565642149420, + 0.000306338738553062, 0.000175970492512707, 0.000080053245121681, 0.000025917992771962], + +2250: # 204 taps +[ 0.000021235929725912, 0.000058222413723091, 0.000120890002831670, 0.000203732920433964, 0.000289842861102114, 0.000349648687783014, + 0.000345987806871638, 0.000244741113281256, 0.000028683359304148, -0.000289767552139220, -0.000662562723513035, -0.001011139906426526, +-0.001244168321321761, -0.001284537444429324, -0.001097853884402359, -0.000712241533727792, -0.000220016931252946, 0.000242881192519691, + 0.000536533139642942, 0.000564406769439812, 0.000312158569581760, -0.000137882856713157, -0.000625674547956830, -0.000963565458358823, +-0.001002878434444682, -0.000693839038801721, -0.000115120712337792, 0.000542271936437241, 0.001036221937634474, 0.001161519435408656, + 0.000833656052982527, 0.000135090190320360, -0.000698564960603252, -0.001354001229403848, -0.001554644716587147, -0.001173297796072647, +-0.000298257519547670, 0.000778476775888895, 0.001654851153636767, 0.001966028879000262, 0.001532563461927731, 0.000450688659966846, +-0.000917352451094544, -0.002059631204322221, -0.002502298531213019, -0.002001396043186186, -0.000662678470838726, 0.001065622033389926, + 0.002537010973466811, 0.003144134768937308, 0.002563969753555681, 0.000913777569993959, -0.001253108006318142, -0.003125545369287028, +-0.003932636886255475, -0.003254230145379617, -0.001221262259301159, 0.001484407116176422, 0.003849603664148359, 0.004902048797404353, + 0.004101215157545026, 0.001593866974392046, -0.001780147556263722, -0.004757480195317273, -0.006114639178429671, -0.005158848135225734, +-0.002053747031888304, 0.002164989106816366, 0.005919348898632932, 0.007665501534134797, 0.006511589945553144, 0.002635680468215082, +-0.002680807848145089, -0.007454309167215744, -0.009718217353005822, -0.008306681703332177, -0.003400058938312392, 0.003406734158392871, + 0.009587959427131761, 0.012589212182082522, 0.010837597719690596, 0.004476080310267183, -0.004494123792760167, -0.012783165957938696, +-0.016952789434463451, -0.014752125292065949, -0.006159094027769298, 0.006304669250227038, 0.018192572127496920, 0.024568955795679859, + 0.021832946865353555, 0.009314262398551900, -0.009958208720588032, -0.029683337395096002, -0.041898689176258344, -0.039357322455930641, +-0.017957925042069021, 0.021558306071190737, 0.073246545073647112, 0.127141924217462950, 0.171715605172332860, 0.196918159377366730, + 0.196918159377366730, 0.171715605172332860, 0.127141924217462950, 0.073246545073647112, 0.021558306071190737, -0.017957925042069021, +-0.039357322455930641, -0.041898689176258344, -0.029683337395096002, -0.009958208720588032, 0.009314262398551900, 0.021832946865353555, + 0.024568955795679859, 0.018192572127496920, 0.006304669250227038, -0.006159094027769298, -0.014752125292065949, -0.016952789434463451, +-0.012783165957938696, -0.004494123792760167, 0.004476080310267183, 0.010837597719690596, 0.012589212182082522, 0.009587959427131761, + 0.003406734158392871, -0.003400058938312392, -0.008306681703332177, -0.009718217353005822, -0.007454309167215744, -0.002680807848145089, + 0.002635680468215082, 0.006511589945553144, 0.007665501534134797, 0.005919348898632932, 0.002164989106816366, -0.002053747031888304, +-0.005158848135225734, -0.006114639178429671, -0.004757480195317273, -0.001780147556263722, 0.001593866974392046, 0.004101215157545026, + 0.004902048797404353, 0.003849603664148359, 0.001484407116176422, -0.001221262259301159, -0.003254230145379617, -0.003932636886255475, +-0.003125545369287028, -0.001253108006318142, 0.000913777569993959, 0.002563969753555681, 0.003144134768937308, 0.002537010973466811, + 0.001065622033389926, -0.000662678470838726, -0.002001396043186186, -0.002502298531213019, -0.002059631204322221, -0.000917352451094544, + 0.000450688659966846, 0.001532563461927731, 0.001966028879000262, 0.001654851153636767, 0.000778476775888895, -0.000298257519547670, +-0.001173297796072647, -0.001554644716587147, -0.001354001229403848, -0.000698564960603252, 0.000135090190320360, 0.000833656052982527, + 0.001161519435408656, 0.001036221937634474, 0.000542271936437241, -0.000115120712337792, -0.000693839038801721, -0.001002878434444682, +-0.000963565458358823, -0.000625674547956830, -0.000137882856713157, 0.000312158569581760, 0.000564406769439812, 0.000536533139642942, + 0.000242881192519691, -0.000220016931252946, -0.000712241533727792, -0.001097853884402359, -0.001284537444429324, -0.001244168321321761, +-0.001011139906426526, -0.000662562723513035, -0.000289767552139220, 0.000028683359304148, 0.000244741113281256, 0.000345987806871638, + 0.000349648687783014, 0.000289842861102114, 0.000203732920433964, 0.000120890002831670, 0.000058222413723091, 0.000021235929725912], + +2200: #208 taps +[ 0.000019773973500698, 0.000050300702719923, 0.000099108356371034, 0.000158194404233062, 0.000210525538441445, 0.000229815943545636, + 0.000185844232898202, 0.000053925802103604, -0.000173777821813056, -0.000478713240844808, -0.000812707500835325, -0.001104087723445511, +-0.001273942998532759, -0.001259278130931017, -0.001036517683714317, -0.000637135855849719, -0.000147905388336950, 0.000307371053002897, + 0.000601888175399474, 0.000646481239850203, 0.000423598448328852, 0.000001477795930511, -0.000479511456398659, -0.000847542574860787, +-0.000956181777710176, -0.000738934578391376, -0.000241404176917473, 0.000383518196967566, 0.000920251953818672, 0.001163208934048226, + 0.000993921481591226, 0.000434388361798019, -0.000346030182304409, -0.001078079759008518, -0.001482742622315739, -0.001374843351537245, +-0.000742498373847161, 0.000230275873300524, 0.001212510588437732, 0.001835971920260912, 0.001830429895718975, 0.001135824361158899, +-0.000053708874146025, -0.001337763115758486, -0.002240751224958943, -0.002384735614959788, -0.001642386517013215, -0.000209813690831605, + 0.001436458922346993, 0.002693390382420342, 0.003048363283850710, 0.002282591868736866, 0.000582131982803615, -0.001494683726987109, +-0.003194641328318874, -0.003837926603637492, -0.003083544121084786, -0.001090420131933939, 0.001496937564140111, 0.003749027796032780, + 0.004778960997800515, 0.004083937494744029, 0.001771796479680179, -0.001424175363221383, -0.004367185919670888, -0.005912778052177615, +-0.005343837748934002, -0.002682898015250491, 0.001248211259214371, 0.005067191417250770, 0.007305293729039890, 0.006958279310681562, + 0.003912446091368335, -0.000927394507475861, -0.005884363717650123, -0.009072440354829070, -0.009093145110623519, -0.005615743824657722, + 0.000389138616762466, 0.006884468473346532, 0.011428522924352827, 0.012058034647345471, 0.008084684337356606, 0.000498409327693868, +-0.008214286092348189, -0.014832606629260737, -0.016526271123526743, -0.011969567139734955, -0.002035744936384088, 0.010229678038446030, + 0.020433030409654745, 0.024256700710114647, 0.019055511377788287, 0.005093243716641166, -0.014044229049919777, -0.032091634104820632, +-0.041707387370849711, -0.036619723239870061, -0.013689386302707808, 0.025758635108220899, 0.075797616878918297, 0.127078744612693170, + 0.169056056140688250, 0.192658606871644660, 0.192658606871644660, 0.169056056140688250, 0.127078744612693170, 0.075797616878918297, + 0.025758635108220899, -0.013689386302707808, -0.036619723239870061, -0.041707387370849711, -0.032091634104820632, -0.014044229049919777, + 0.005093243716641166, 0.019055511377788287, 0.024256700710114647, 0.020433030409654745, 0.010229678038446030, -0.002035744936384088, +-0.011969567139734955, -0.016526271123526743, -0.014832606629260737, -0.008214286092348189, 0.000498409327693868, 0.008084684337356606, + 0.012058034647345471, 0.011428522924352827, 0.006884468473346532, 0.000389138616762466, -0.005615743824657722, -0.009093145110623519, +-0.009072440354829070, -0.005884363717650123, -0.000927394507475861, 0.003912446091368335, 0.006958279310681562, 0.007305293729039890, + 0.005067191417250770, 0.001248211259214371, -0.002682898015250491, -0.005343837748934002, -0.005912778052177615, -0.004367185919670888, +-0.001424175363221383, 0.001771796479680179, 0.004083937494744029, 0.004778960997800515, 0.003749027796032780, 0.001496937564140111, +-0.001090420131933939, -0.003083544121084786, -0.003837926603637492, -0.003194641328318874, -0.001494683726987109, 0.000582131982803615, + 0.002282591868736866, 0.003048363283850710, 0.002693390382420342, 0.001436458922346993, -0.000209813690831605, -0.001642386517013215, +-0.002384735614959788, -0.002240751224958943, -0.001337763115758486, -0.000053708874146025, 0.001135824361158899, 0.001830429895718975, + 0.001835971920260912, 0.001212510588437732, 0.000230275873300524, -0.000742498373847161, -0.001374843351537245, -0.001482742622315739, +-0.001078079759008518, -0.000346030182304409, 0.000434388361798019, 0.000993921481591226, 0.001163208934048226, 0.000920251953818672, + 0.000383518196967566, -0.000241404176917473, -0.000738934578391376, -0.000956181777710176, -0.000847542574860787, -0.000479511456398659, + 0.000001477795930511, 0.000423598448328852, 0.000646481239850203, 0.000601888175399474, 0.000307371053002897, -0.000147905388336950, +-0.000637135855849719, -0.001036517683714317, -0.001259278130931017, -0.001273942998532759, -0.001104087723445511, -0.000812707500835325, +-0.000478713240844808, -0.000173777821813056, 0.000053925802103604, 0.000185844232898202, 0.000229815943545636, 0.000210525538441445, + 0.000158194404233062, 0.000099108356371034, 0.000050300702719923, 0.000019773973500698], + +2000: # 230 taps +[ 0.000018263332334824, 0.000047149978647285, 0.000097406241243338, 0.000168379817473922, 0.000253967146823514, 0.000340023431940833, + 0.000404952972379638, 0.000422974737428721, 0.000369902505736477, 0.000230539284736893, 0.000005684130024729, -0.000283317092531425, +-0.000594762485225633, -0.000872546941267996, -0.001057521343918215, -0.001102228302856328, -0.000985222996371773, -0.000720881682050162, +-0.000360902436481765, 0.000014352678995945, 0.000313854754768513, 0.000459361314264358, 0.000408357201159471, 0.000169601665199639, +-0.000194309748856567, -0.000579769259034295, -0.000868450605965827, -0.000960850467769566, -0.000807759775150557, -0.000429923379846805, + 0.000081456338895501, 0.000585585033204507, 0.000930280363007829, 0.000997553630123726, 0.000743304377770887, 0.000218142932019430, +-0.000439021999187466, -0.001035464584506922, -0.001379274582853274, -0.001339118806894442, -0.000890898584125663, -0.000135426700809927, + 0.000721291534689607, 0.001422586528925900, 0.001736709146196323, 0.001531678957580429, 0.000825809348346554, -0.000204332584606896, +-0.001264953954035041, -0.002026681147110289, -0.002224767812024736, -0.001747989347578871, -0.000687126008900549, 0.000675855233958752, + 0.001939810141526326, 0.002700254906709327, 0.002676337670079708, 0.001809809227907344, 0.000302346385971505, -0.001425894059131038, +-0.002850111164443048, -0.003497344177823653, -0.003099785097153715, -0.001696282588251145, 0.000354381431382131, 0.002459975729847483, + 0.003962117890550910, 0.004338291351008917, 0.003377760857642002, 0.001273722107127891, -0.001404260811957126, -0.003858729944018619, +-0.005296868477641261, -0.005180794578143301, -0.003418238131210470, -0.000428690403667462, 0.002946095146143898, 0.005671970307130408, + 0.006832059383649537, 0.005925580242615656, 0.003061135525037048, -0.001025055462746871, -0.005146493168885704, -0.008000447007066858, +-0.008567643322000875, -0.006458387751298429, -0.002092389293377035, 0.003358433615988978, 0.008267542643372157, 0.011021009169962534, + 0.010525039504447272, 0.006600477461859877, 0.000131874225722409, -0.007098295196117369, -0.012870086951713777, -0.015169746940300757, +-0.012839241513492812, -0.006035689378457242, 0.003657448927275838, 0.013537787301098309, 0.020464471653522786, 0.021753420096582588, + 0.016052407125049746, 0.003944898543127921, -0.011915101650258511, -0.027222337492958711, -0.036934437155297005, -0.036483323022197825, +-0.022992339899140058, 0.003814121297142671, 0.041261656200314752, 0.084061156150449762, 0.125284573669860120, 0.157774677964436120, + 0.175667194419142110, 0.175667194419142110, 0.157774677964436120, 0.125284573669860120, 0.084061156150449762, 0.041261656200314752, + 0.003814121297142671, -0.022992339899140058, -0.036483323022197825, -0.036934437155297005, -0.027222337492958711, -0.011915101650258511, + 0.003944898543127921, 0.016052407125049746, 0.021753420096582588, 0.020464471653522786, 0.013537787301098309, 0.003657448927275838, +-0.006035689378457242, -0.012839241513492812, -0.015169746940300757, -0.012870086951713777, -0.007098295196117369, 0.000131874225722409, + 0.006600477461859877, 0.010525039504447272, 0.011021009169962534, 0.008267542643372157, 0.003358433615988978, -0.002092389293377035, +-0.006458387751298429, -0.008567643322000875, -0.008000447007066858, -0.005146493168885704, -0.001025055462746871, 0.003061135525037048, + 0.005925580242615656, 0.006832059383649537, 0.005671970307130408, 0.002946095146143898, -0.000428690403667462, -0.003418238131210470, +-0.005180794578143301, -0.005296868477641261, -0.003858729944018619, -0.001404260811957126, 0.001273722107127891, 0.003377760857642002, + 0.004338291351008917, 0.003962117890550910, 0.002459975729847483, 0.000354381431382131, -0.001696282588251145, -0.003099785097153715, +-0.003497344177823653, -0.002850111164443048, -0.001425894059131038, 0.000302346385971505, 0.001809809227907344, 0.002676337670079708, + 0.002700254906709327, 0.001939810141526326, 0.000675855233958752, -0.000687126008900549, -0.001747989347578871, -0.002224767812024736, +-0.002026681147110289, -0.001264953954035041, -0.000204332584606896, 0.000825809348346554, 0.001531678957580429, 0.001736709146196323, + 0.001422586528925900, 0.000721291534689607, -0.000135426700809927, -0.000890898584125663, -0.001339118806894442, -0.001379274582853274, +-0.001035464584506922, -0.000439021999187466, 0.000218142932019430, 0.000743304377770887, 0.000997553630123726, 0.000930280363007829, + 0.000585585033204507, 0.000081456338895501, -0.000429923379846805, -0.000807759775150557, -0.000960850467769566, -0.000868450605965827, +-0.000579769259034295, -0.000194309748856567, 0.000169601665199639, 0.000408357201159471, 0.000459361314264358, 0.000313854754768513, + 0.000014352678995945, -0.000360902436481765, -0.000720881682050162, -0.000985222996371773, -0.001102228302856328, -0.001057521343918215, +-0.000872546941267996, -0.000594762485225633, -0.000283317092531425, 0.000005684130024729, 0.000230539284736893, 0.000369902505736477, + 0.000422974737428721, 0.000404952972379638, 0.000340023431940833, 0.000253967146823514, 0.000168379817473922, 0.000097406241243338, + 0.000047149978647285, 0.000018263332334824], + +1200: # 390 taps +[ -0.000004331430434813, 0.000000895178754483, 0.000004819206934844, 0.000012387135981227, 0.000024702874344864, 0.000042904801446151, + 0.000067985064859609, 0.000100627030615644, 0.000141025873402660, 0.000188738905083740, 0.000242545741476270, 0.000300399529662751, + 0.000359433330154281, 0.000416062388387887, 0.000466131771574125, 0.000505276256945394, 0.000529201721306850, 0.000534142706869689, + 0.000517270879752382, 0.000477091889474874, 0.000413779420856055, 0.000329374266276144, 0.000227844055373822, 0.000114956874548630, +-0.000002037964985788, -0.000114915331771079, -0.000215124321927526, -0.000294538502564660, -0.000346261198238618, -0.000365357958551737, +-0.000349492174517184, -0.000299346594446952, -0.000218781210350266, -0.000114696259371792, 0.000003439467944363, 0.000124342943768293, + 0.000235923038322712, 0.000326439472488279, 0.000385716174095538, 0.000406270345323467, 0.000384244892423745, 0.000320026589486952, + 0.000218451713387220, 0.000088560889198400, -0.000057113100623293, -0.000203679685243764, -0.000335341513050654, -0.000437048860128537, +-0.000496163931220179, -0.000503976472887228, -0.000456879139659229, -0.000357064175200153, -0.000212619491029857, -0.000036988477009561, + 0.000152196470731637, 0.000334813551887072, 0.000490329017689163, 0.000600045160004744, 0.000649282295414473, 0.000629236561431697, + 0.000538304340843976, 0.000382670778203303, 0.000176071280391891, -0.000061311579103233, -0.000304731538051063, -0.000527388822625111, +-0.000703310824877695, -0.000810309677712974, -0.000832681816046785, -0.000763333939111765, -0.000605059771056946, -0.000370777976208385, +-0.000082637093413831, 0.000229962373724818, 0.000533282556646377, 0.000792807201162301, 0.000977090407652497, 0.001061465563886132, + 0.001031195280476629, 0.000883666665500652, 0.000629337435771274, 0.000291251466919408, -0.000096894708513463, -0.000493996123025552, +-0.000855794020410478, -0.001139717153414092, -0.001309782074955667, -0.001341010390430392, -0.001222824367769252, -0.000960990590585683, +-0.000577807284515617, -0.000110428182918688, 0.000392587860975165, 0.000876197024190483, 0.001284780403728217, 0.001568419571371085, + 0.001688844037687690, 0.001624356624161237, 0.001373126810656710, 0.000954389812320343, 0.000407316271842430, -0.000212443049446438, +-0.000838254589610985, -0.001399522686014101, -0.001829507546348522, -0.002073092512801241, -0.002093612886275790, -0.001877924713394541, +-0.001439048979367570, -0.000815971265919441, -0.000070480695977658, 0.000718736847294852, 0.001464215661002276, 0.002079218519234449, + 0.002487627013869813, 0.002633104169545422, 0.002486455189532861, 0.002050261072858978, 0.001360126142354939, 0.000482234298234895, +-0.000492687418387142, -0.001458485408297803, -0.002304891424791415, -0.002929849705482365, -0.003251510669015779, -0.003218513620108414, +-0.002817308401190046, -0.002075543578763564, -0.001060944493273319, 0.000124409621994965, 0.001353984612741832, 0.002489950284201132, + 0.003398266860636230, 0.003964087731928019, 0.004105749968228098, 0.003785703607610913, 0.003016991173328030, 0.001864327522378759, + 0.000439388406465501, -0.001109448711112102, -0.002612032761559656, -0.003894589690524808, -0.004799215003224437, -0.005202582868054837, +-0.005031731058566960, -0.004274996752467488, -0.002986628326221043, -0.001284225000364678, 0.000661081413588171, 0.002640990158453115, + 0.004431457793095885, 0.005817070614785085, 0.006615805917154524, 0.006701450960612391, 0.006021062146242219, 0.004605255832862724, + 0.002569791244804701, 0.000107781839820803, -0.002527142979701336, -0.005045394324250144, -0.007151459446961864, -0.008576455016121923, +-0.009109805876143171, -0.008626557809676316, -0.007107110348255605, -0.004646780506669336, -0.001453545516455117, 0.002166532839260334, + 0.005835432981396591, 0.009138830213137229, 0.011668164700843125, 0.013065169962452085, 0.013064440018490608, 0.011529579182106710, + 0.008478886219133458, 0.004097368728152922, -0.001266921998658053, -0.007122790428060381, -0.012871021717463436, -0.017852189458389456, +-0.021403262319433757, -0.022918110896739993, -0.021906440359153793, -0.018045605353792606, -0.011220225882222989, -0.001545481108326266, + 0.010628660576177286, 0.024733344647961189, 0.040016116357312823, 0.055593053292742431, 0.070513617769391357, 0.083832869405100041, + 0.094684919293335654, 0.102351302009811070, 0.106318317517167070, 0.106318317517167070, 0.102351302009811070, 0.094684919293335654, + 0.083832869405100041, 0.070513617769391357, 0.055593053292742431, 0.040016116357312823, 0.024733344647961189, 0.010628660576177286, +-0.001545481108326266, -0.011220225882222989, -0.018045605353792606, -0.021906440359153793, -0.022918110896739993, -0.021403262319433757, +-0.017852189458389456, -0.012871021717463436, -0.007122790428060381, -0.001266921998658053, 0.004097368728152922, 0.008478886219133458, + 0.011529579182106710, 0.013064440018490608, 0.013065169962452085, 0.011668164700843125, 0.009138830213137229, 0.005835432981396591, + 0.002166532839260334, -0.001453545516455117, -0.004646780506669336, -0.007107110348255605, -0.008626557809676316, -0.009109805876143171, +-0.008576455016121923, -0.007151459446961864, -0.005045394324250144, -0.002527142979701336, 0.000107781839820803, 0.002569791244804701, + 0.004605255832862724, 0.006021062146242219, 0.006701450960612391, 0.006615805917154524, 0.005817070614785085, 0.004431457793095885, + 0.002640990158453115, 0.000661081413588171, -0.001284225000364678, -0.002986628326221043, -0.004274996752467488, -0.005031731058566960, +-0.005202582868054837, -0.004799215003224437, -0.003894589690524808, -0.002612032761559656, -0.001109448711112102, 0.000439388406465501, + 0.001864327522378759, 0.003016991173328030, 0.003785703607610913, 0.004105749968228098, 0.003964087731928019, 0.003398266860636230, + 0.002489950284201132, 0.001353984612741832, 0.000124409621994965, -0.001060944493273319, -0.002075543578763564, -0.002817308401190046, +-0.003218513620108414, -0.003251510669015779, -0.002929849705482365, -0.002304891424791415, -0.001458485408297803, -0.000492687418387142, + 0.000482234298234895, 0.001360126142354939, 0.002050261072858978, 0.002486455189532861, 0.002633104169545422, 0.002487627013869813, + 0.002079218519234449, 0.001464215661002276, 0.000718736847294852, -0.000070480695977658, -0.000815971265919441, -0.001439048979367570, +-0.001877924713394541, -0.002093612886275790, -0.002073092512801241, -0.001829507546348522, -0.001399522686014101, -0.000838254589610985, +-0.000212443049446438, 0.000407316271842430, 0.000954389812320343, 0.001373126810656710, 0.001624356624161237, 0.001688844037687690, + 0.001568419571371085, 0.001284780403728217, 0.000876197024190483, 0.000392587860975165, -0.000110428182918688, -0.000577807284515617, +-0.000960990590585683, -0.001222824367769252, -0.001341010390430392, -0.001309782074955667, -0.001139717153414092, -0.000855794020410478, +-0.000493996123025552, -0.000096894708513463, 0.000291251466919408, 0.000629337435771274, 0.000883666665500652, 0.001031195280476629, + 0.001061465563886132, 0.000977090407652497, 0.000792807201162301, 0.000533282556646377, 0.000229962373724818, -0.000082637093413831, +-0.000370777976208385, -0.000605059771056946, -0.000763333939111765, -0.000832681816046785, -0.000810309677712974, -0.000703310824877695, +-0.000527388822625111, -0.000304731538051063, -0.000061311579103233, 0.000176071280391891, 0.000382670778203303, 0.000538304340843976, + 0.000629236561431697, 0.000649282295414473, 0.000600045160004744, 0.000490329017689163, 0.000334813551887072, 0.000152196470731637, +-0.000036988477009561, -0.000212619491029857, -0.000357064175200153, -0.000456879139659229, -0.000503976472887228, -0.000496163931220179, +-0.000437048860128537, -0.000335341513050654, -0.000203679685243764, -0.000057113100623293, 0.000088560889198400, 0.000218451713387220, + 0.000320026589486952, 0.000384244892423745, 0.000406270345323467, 0.000385716174095538, 0.000326439472488279, 0.000235923038322712, + 0.000124342943768293, 0.000003439467944363, -0.000114696259371792, -0.000218781210350266, -0.000299346594446952, -0.000349492174517184, +-0.000365357958551737, -0.000346261198238618, -0.000294538502564660, -0.000215124321927526, -0.000114915331771079, -0.000002037964985788, + 0.000114956874548630, 0.000227844055373822, 0.000329374266276144, 0.000413779420856055, 0.000477091889474874, 0.000517270879752382, + 0.000534142706869689, 0.000529201721306850, 0.000505276256945394, 0.000466131771574125, 0.000416062388387887, 0.000359433330154281, + 0.000300399529662751, 0.000242545741476270, 0.000188738905083740, 0.000141025873402660, 0.000100627030615644, 0.000067985064859609, + 0.000042904801446151, 0.000024702874344864, 0.000012387135981227, 0.000004819206934844, 0.000000895178754483, +-0.000004331430434813], + +1050: # 436 taps +[ 0.000009448479205375, 0.000012696465277652, 0.000020370172970180, 0.000030184991625035, 0.000042106693955817, 0.000055852072599971, + 0.000070887005271560, 0.000086420158465092, 0.000101355825900618, 0.000114363268643203, 0.000123887834821102, 0.000128274519298202, + 0.000125832586450277, 0.000114986590640600, 0.000094403728281619, 0.000063154754823051, 0.000020845988244699, -0.000032265535133044, +-0.000095180544123547, -0.000166116860144892, -0.000242513979646880, -0.000321114777409093, -0.000398098262223401, -0.000469277292459888, +-0.000530343005106164, -0.000577150816605647, -0.000606030552867816, -0.000614088176959215, -0.000599488135042163, -0.000561676165437471, +-0.000501537574188219, -0.000421453273437500, -0.000325256102322617, -0.000218069429252057, -0.000106038676866598, 0.000004038733321948, + 0.000105163201801783, 0.000190629395147399, 0.000254537701320533, 0.000292276183024851, 0.000300940504739341, 0.000279647367467038, + 0.000229712182001491, 0.000154664133559244, 0.000060087287885032, -0.000046708916188273, -0.000157178278901246, -0.000262150604318790, +-0.000352532643004546, -0.000420038843447295, -0.000457900791044766, -0.000461490220027360, -0.000428802562294159, -0.000360749682491985, +-0.000261227783830262, -0.000136942941254748, 0.000003006863095541, 0.000147769782466689, 0.000285560076794725, 0.000404597750694492, + 0.000494097819748234, 0.000545227040891837, 0.000551946387992179, 0.000511661803626807, 0.000425615907406962, 0.000298975923041507, + 0.000140591237099924, -0.000037572806623586, -0.000221297040657601, -0.000395204977572457, -0.000544014582709044, -0.000653844664412656, +-0.000713468032359235, -0.000715401015700932, -0.000656726220245329, -0.000539565358551840, -0.000371142263662183, -0.000163412087825843, + 0.000067734912716773, 0.000303646478822936, 0.000524376688359045, 0.000710337247559984, 0.000843997687273033, 0.000911491282666008, + 0.000903981565989860, 0.000818661581901103, 0.000659279571163340, 0.000436124965687766, 0.000165448090041410, -0.000131662114271814, +-0.000430871860061819, -0.000706551570190628, -0.000933927661174878, -0.001091260873927019, -0.001161862605544963, -0.001135768011733947, +-0.001010903443574116, -0.000793624567958766, -0.000498548786522930, -0.000147664383619282, 0.000231239373696082, 0.000606716500508849, + 0.000946221796915500, 0.001218879442233109, 0.001398230278516649, 0.001464719587961910, 0.001407696084540016, 0.001226727670795367, + 0.000932088534687613, 0.000544338945220889, 0.000092993997809168, -0.000385643796196990, -0.000851335887551482, -0.001263225987480693, +-0.001583356294077108, -0.001780085545791869, -0.001831106846925676, -0.001725785932705194, -0.001466586997062531, -0.001069422391909016, +-0.000562849829295198, 0.000013863768127086, 0.000613691289313864, 0.001185595579630164, 0.001678788016865224, 0.002047148219684201, + 0.002253427125685210, 0.002272859461156746, 0.002095846355480220, 0.001729435227991120, 0.001197417642361670, 0.000538977181181343, +-0.000194056307018250, -0.000941167802354134, -0.001638040755694804, -0.002221979108638823, -0.002637419881293469, -0.002841061961880444, +-0.002806150254550888, -0.002525507242008391, -0.002012996612957378, -0.001303224500472842, -0.000449429868884502, 0.000480331461610398, + 0.001408451066145862, 0.002254087235765462, 0.002940037601874124, 0.003399594478686270, 0.003582789834066899, 0.003461463426614760, + 0.003032665933951381, 0.002320030370388926, 0.001372905661370613, 0.000263228943665216, -0.000919692494408471, -0.002076132077760251, +-0.003104105713267222, -0.003908095577429846, -0.004407611415610481, -0.004544845892385366, -0.004290726778249251, -0.003648773776410054, +-0.002656330572825240, -0.001382946510100638, 0.000074084090594797, 0.001596775046947397, 0.003055612326596284, 0.004320145989574181, + 0.005270175821128589, 0.005806607129742208, 0.005861039774711523, 0.005403216045538973, 0.004445593651759434, 0.003044516829901583, + 0.001297719390662469, -0.000661812043684862, -0.002675281726881974, -0.004570210544203085, -0.006174367519919625, -0.007330437981090785, +-0.007910248018541601, -0.007827342889142773, -0.007046794045290511, -0.005591280073358511, -0.003542743024648185, -0.001039245490340182, + 0.001732978782646685, 0.004551902468552846, 0.007175761837551073, 0.009361715988035382, 0.010885774625277341, 0.011562497118639900, + 0.011262913292836945, 0.009929181902797219, 0.007584685668611571, 0.004338552786979482, 0.000383973096667143, -0.004009880384869925, +-0.008512042746918275, -0.012749310695913854, -0.016329981037081505, -0.018870242377519306, -0.020021437259417553, -0.019496284710171480, +-0.017092162548070899, -0.012709698776261012, -0.006365201717689276, 0.001804145292512615, 0.011542917413936767, 0.022487965990827675, + 0.034186326021837028, 0.046119492420907078, 0.057732581500000407, 0.068466529917194638, 0.077791244144850280, 0.085237520364008251, + 0.090425612489785534, 0.093088535620340640, 0.093088535620340640, 0.090425612489785534, 0.085237520364008251, 0.077791244144850280, + 0.068466529917194638, 0.057732581500000407, 0.046119492420907078, 0.034186326021837028, 0.022487965990827675, 0.011542917413936767, + 0.001804145292512615, -0.006365201717689276, -0.012709698776261012, -0.017092162548070899, -0.019496284710171480, -0.020021437259417553, +-0.018870242377519306, -0.016329981037081505, -0.012749310695913854, -0.008512042746918275, -0.004009880384869925, 0.000383973096667143, + 0.004338552786979482, 0.007584685668611571, 0.009929181902797219, 0.011262913292836945, 0.011562497118639900, 0.010885774625277341, + 0.009361715988035382, 0.007175761837551073, 0.004551902468552846, 0.001732978782646685, -0.001039245490340182, -0.003542743024648185, +-0.005591280073358511, -0.007046794045290511, -0.007827342889142773, -0.007910248018541601, -0.007330437981090785, -0.006174367519919625, +-0.004570210544203085, -0.002675281726881974, -0.000661812043684862, 0.001297719390662469, 0.003044516829901583, 0.004445593651759434, + 0.005403216045538973, 0.005861039774711523, 0.005806607129742208, 0.005270175821128589, 0.004320145989574181, 0.003055612326596284, + 0.001596775046947397, 0.000074084090594797, -0.001382946510100638, -0.002656330572825240, -0.003648773776410054, -0.004290726778249251, +-0.004544845892385366, -0.004407611415610481, -0.003908095577429846, -0.003104105713267222, -0.002076132077760251, -0.000919692494408471, + 0.000263228943665216, 0.001372905661370613, 0.002320030370388926, 0.003032665933951381, 0.003461463426614760, 0.003582789834066899, + 0.003399594478686270, 0.002940037601874124, 0.002254087235765462, 0.001408451066145862, 0.000480331461610398, -0.000449429868884502, +-0.001303224500472842, -0.002012996612957378, -0.002525507242008391, -0.002806150254550888, -0.002841061961880444, -0.002637419881293469, +-0.002221979108638823, -0.001638040755694804, -0.000941167802354134, -0.000194056307018250, 0.000538977181181343, 0.001197417642361670, + 0.001729435227991120, 0.002095846355480220, 0.002272859461156746, 0.002253427125685210, 0.002047148219684201, 0.001678788016865224, + 0.001185595579630164, 0.000613691289313864, 0.000013863768127086, -0.000562849829295198, -0.001069422391909016, -0.001466586997062531, +-0.001725785932705194, -0.001831106846925676, -0.001780085545791869, -0.001583356294077108, -0.001263225987480693, -0.000851335887551482, +-0.000385643796196990, 0.000092993997809168, 0.000544338945220889, 0.000932088534687613, 0.001226727670795367, 0.001407696084540016, + 0.001464719587961910, 0.001398230278516649, 0.001218879442233109, 0.000946221796915500, 0.000606716500508849, 0.000231239373696082, +-0.000147664383619282, -0.000498548786522930, -0.000793624567958766, -0.001010903443574116, -0.001135768011733947, -0.001161862605544963, +-0.001091260873927019, -0.000933927661174878, -0.000706551570190628, -0.000430871860061819, -0.000131662114271814, 0.000165448090041410, + 0.000436124965687766, 0.000659279571163340, 0.000818661581901103, 0.000903981565989860, 0.000911491282666008, 0.000843997687273033, + 0.000710337247559984, 0.000524376688359045, 0.000303646478822936, 0.000067734912716773, -0.000163412087825843, -0.000371142263662183, +-0.000539565358551840, -0.000656726220245329, -0.000715401015700932, -0.000713468032359235, -0.000653844664412656, -0.000544014582709044, +-0.000395204977572457, -0.000221297040657601, -0.000037572806623586, 0.000140591237099924, 0.000298975923041507, 0.000425615907406962, + 0.000511661803626807, 0.000551946387992179, 0.000545227040891837, 0.000494097819748234, 0.000404597750694492, 0.000285560076794725, + 0.000147769782466689, 0.000003006863095541, -0.000136942941254748, -0.000261227783830262, -0.000360749682491985, -0.000428802562294159, +-0.000461490220027360, -0.000457900791044766, -0.000420038843447295, -0.000352532643004546, -0.000262150604318790, -0.000157178278901246, +-0.000046708916188273, 0.000060087287885032, 0.000154664133559244, 0.000229712182001491, 0.000279647367467038, 0.000300940504739341, + 0.000292276183024851, 0.000254537701320533, 0.000190629395147399, 0.000105163201801783, 0.000004038733321948, -0.000106038676866598, +-0.000218069429252057, -0.000325256102322617, -0.000421453273437500, -0.000501537574188219, -0.000561676165437471, -0.000599488135042163, +-0.000614088176959215, -0.000606030552867816, -0.000577150816605647, -0.000530343005106164, -0.000469277292459888, -0.000398098262223401, +-0.000321114777409093, -0.000242513979646880, -0.000166116860144892, -0.000095180544123547, -0.000032265535133044, 0.000020845988244699, + 0.000063154754823051, 0.000094403728281619, 0.000114986590640600, 0.000125832586450277, 0.000128274519298202, 0.000123887834821102, + 0.000114363268643203, 0.000101355825900618, 0.000086420158465092, 0.000070887005271560, 0.000055852072599971, 0.000042106693955817, + 0.000030184991625035, 0.000020370172970180, 0.000012696465277652, 0.000009448479205375 ], + +900: # 507 taps +[ 0.000008584643858565, 0.000009418988876053, 0.000014080571079703, 0.000019768330546629, 0.000026407938202815, 0.000033835554436160, + 0.000041775447258916, 0.000049840688763229, 0.000057525397104827, 0.000064222904381994, 0.000069229653331724, 0.000071774979399342, + 0.000071044828173283, 0.000066239730112684, 0.000056611414853593, 0.000041517869163367, 0.000020462723568921, -0.000006825518135279, +-0.000040362137084292, -0.000079866391614059, -0.000124740944604127, -0.000174014477804677, -0.000226421731946560, -0.000280359514663211, +-0.000333959777524947, -0.000385165398367582, -0.000431779497772114, -0.000471602831913601, -0.000502508714800933, -0.000522587601943708, +-0.000530244985591953, -0.000524326246532992, -0.000504199216087390, -0.000469843487184555, -0.000421891912076977, -0.000361653837423190, +-0.000291090671787090, -0.000212772068148448, -0.000129775708505696, -0.000045562167590561, 0.000036181038541454, 0.000111715248667983, + 0.000177457791675865, 0.000230168155042476, 0.000267145694421862, 0.000286408115238973, 0.000286822428035563, 0.000268225243572190, + 0.000231462231911356, 0.000178399161339807, 0.000111858949442223, 0.000035506782941005, -0.000046322038661538, -0.000128840476454010, +-0.000207064338646769, -0.000276095967153748, -0.000331410885812673, -0.000369138410006413, -0.000386317254135060, -0.000381111227069818, +-0.000352960919521696, -0.000302673831926362, -0.000232432591548855, -0.000145719497401035, -0.000047160366530124, 0.000057705644340284, + 0.000162731306278391, 0.000261502601122459, 0.000347728839468377, 0.000415628506871784, 0.000460307676423158, 0.000478101121808084, + 0.000466848645480576, 0.000426095215060785, 0.000357188814988566, 0.000263277817392097, 0.000149189135268233, 0.000021201177134175, +-0.000113285445089856, -0.000246159846025947, -0.000369078758947477, -0.000473974817394057, -0.000553573238187010, -0.000601879769614216, +-0.000614609034335983, -0.000589524295786869, -0.000526664670957786, -0.000428434330673012, -0.000299553787970989, -0.000146857332295172, + 0.000021049474537857, 0.000194254912976452, 0.000362113995846136, 0.000513885517380605, 0.000639407293886249, 0.000729757323783321, + 0.000777868837348859, 0.000779050717176191, 0.000731379294218185, 0.000635932028877494, 0.000496840283261992, 0.000321150767788200, + 0.000118496954373743, -0.000099406083911376, -0.000319420735859412, -0.000527759843251443, -0.000710834560643461, -0.000856122360814998, +-0.000953000983271033, -0.000993496322190293, -0.000972892522850171, -0.000890155314802122, -0.000748144004291994, -0.000553578640569728, +-0.000316765531771492, -0.000051080037307638, 0.000227762682493944, 0.000502615360611366, 0.000755932794801656, 0.000970869777260266, + 0.001132378198325256, 0.001228230831826114, 0.001249905234811852, 0.001193265605753869, 0.001058991562763383, 0.000852715731034719, + 0.000584852833903113, 0.000270116979072993, -0.000073253806782210, -0.000424524625176341, -0.000761689644626737, -0.001062825395605405, +-0.001307487536115730, -0.001478067477842081, -0.001561017661066179, -0.001547863924535660, -0.001435933022484648, -0.001228735487931008, +-0.000935971757611233, -0.000573140492907433, -0.000160766721791647, 0.000276722077340107, 0.000712407276705890, 0.001118502589392694, + 0.001468091403060663, 0.001736873763731978, 0.001904808530758137, 0.001957546402581353, 0.001887551700267456, 0.001694830920708386, + 0.001387207697842587, 0.000980110478290227, 0.000495866308669078, -0.000037468525946086, -0.000587687238352686, -0.001120332197278825, +-0.001600807439724522, -0.001996577848597569, -0.002279316251738701, -0.002426862344229953, -0.002424859679572050, -0.002267956637237035, +-0.001960478287612480, -0.001516506229070831, -0.000959342574003313, -0.000320365808444990, 0.000362667830279848, 0.001047791955533175, + 0.001691344168593488, 0.002250683068847439, 0.002686938772729870, 0.002967628123966539, 0.003068960407414939, 0.002977678084347548, + 0.002692297230438709, 0.002223648489886749, 0.001594655540313720, 0.000839340735107497, 0.000001086828787463, -0.000869758241867305, +-0.001718811255366889, -0.002490949831033119, -0.003133783611782112, -0.003601086428852005, -0.003855971828191669, -0.003873602941247202, +-0.003643247691842578, -0.003169524021369342, -0.002472727031941658, -0.001588180215901802, -0.000564613998354494, 0.000538365561072565, + 0.001653594591084052, 0.002710281216575659, 0.003638323695477434, 0.004372765950231630, 0.004858115780200945, 0.005052252604909589, + 0.004929664405729861, 0.004483785715272022, 0.003728252839970840, 0.002696955568587015, 0.001442828675796044, 0.000035407607162536, +-0.001442758217956277, -0.002900658243916532, -0.004244107302544382, -0.005381470691504941, -0.006229500241120763, -0.006718928923729580, +-0.006799475746570288, -0.006443928763042613, -0.005651020748503490, -0.004446866818108242, -0.002884813426831026, -0.001043632901391050, + 0.000975905251500855, 0.003055961712387741, 0.005067831037893753, 0.006879122081429573, 0.008361541203410547, 0.009398858161408168, + 0.009894604643011696, 0.009779054490283380, 0.009015056125786116, 0.007602336538990519, 0.005579966604992522, 0.003026770185465134, + 0.000059562008821989, -0.003170779553680069, -0.006485285707743206, -0.009684675209522759, -0.012558594763991068, -0.014895884693277550, +-0.016495367870764655, -0.017176610952606329, -0.016790098400516502, -0.015226268406232360, -0.012422909599680294, -0.008370486626783004, +-0.003115063791803408, 0.003241392133040441, 0.010543411361223271, 0.018587073767192364, 0.027127521275133942, 0.035888613808298719, + 0.044574291632219959, 0.052881118780651611, 0.060511410978741485, 0.067186317405564938, 0.072658219055817971, 0.076721836801003668, + 0.079223499565701364, 0.080068115845938093, 0.079223499565701364, 0.076721836801003668, 0.072658219055817971, 0.067186317405564938, + 0.060511410978741485, 0.052881118780651611, 0.044574291632219959, 0.035888613808298719, 0.027127521275133942, 0.018587073767192364, + 0.010543411361223271, 0.003241392133040441, -0.003115063791803408, -0.008370486626783004, -0.012422909599680294, -0.015226268406232360, +-0.016790098400516502, -0.017176610952606329, -0.016495367870764655, -0.014895884693277550, -0.012558594763991068, -0.009684675209522759, +-0.006485285707743206, -0.003170779553680069, 0.000059562008821989, 0.003026770185465134, 0.005579966604992522, 0.007602336538990519, + 0.009015056125786116, 0.009779054490283380, 0.009894604643011696, 0.009398858161408168, 0.008361541203410547, 0.006879122081429573, + 0.005067831037893753, 0.003055961712387741, 0.000975905251500855, -0.001043632901391050, -0.002884813426831026, -0.004446866818108242, +-0.005651020748503490, -0.006443928763042613, -0.006799475746570288, -0.006718928923729580, -0.006229500241120763, -0.005381470691504941, +-0.004244107302544382, -0.002900658243916532, -0.001442758217956277, 0.000035407607162536, 0.001442828675796044, 0.002696955568587015, + 0.003728252839970840, 0.004483785715272022, 0.004929664405729861, 0.005052252604909589, 0.004858115780200945, 0.004372765950231630, + 0.003638323695477434, 0.002710281216575659, 0.001653594591084052, 0.000538365561072565, -0.000564613998354494, -0.001588180215901802, +-0.002472727031941658, -0.003169524021369342, -0.003643247691842578, -0.003873602941247202, -0.003855971828191669, -0.003601086428852005, +-0.003133783611782112, -0.002490949831033119, -0.001718811255366889, -0.000869758241867305, 0.000001086828787463, 0.000839340735107497, + 0.001594655540313720, 0.002223648489886749, 0.002692297230438709, 0.002977678084347548, 0.003068960407414939, 0.002967628123966539, + 0.002686938772729870, 0.002250683068847439, 0.001691344168593488, 0.001047791955533175, 0.000362667830279848, -0.000320365808444990, +-0.000959342574003313, -0.001516506229070831, -0.001960478287612480, -0.002267956637237035, -0.002424859679572050, -0.002426862344229953, +-0.002279316251738701, -0.001996577848597569, -0.001600807439724522, -0.001120332197278825, -0.000587687238352686, -0.000037468525946086, + 0.000495866308669078, 0.000980110478290227, 0.001387207697842587, 0.001694830920708386, 0.001887551700267456, 0.001957546402581353, + 0.001904808530758137, 0.001736873763731978, 0.001468091403060663, 0.001118502589392694, 0.000712407276705890, 0.000276722077340107, +-0.000160766721791647, -0.000573140492907433, -0.000935971757611233, -0.001228735487931008, -0.001435933022484648, -0.001547863924535660, +-0.001561017661066179, -0.001478067477842081, -0.001307487536115730, -0.001062825395605405, -0.000761689644626737, -0.000424524625176341, +-0.000073253806782210, 0.000270116979072993, 0.000584852833903113, 0.000852715731034719, 0.001058991562763383, 0.001193265605753869, + 0.001249905234811852, 0.001228230831826114, 0.001132378198325256, 0.000970869777260266, 0.000755932794801656, 0.000502615360611366, + 0.000227762682493944, -0.000051080037307638, -0.000316765531771492, -0.000553578640569728, -0.000748144004291994, -0.000890155314802122, +-0.000972892522850171, -0.000993496322190293, -0.000953000983271033, -0.000856122360814998, -0.000710834560643461, -0.000527759843251443, +-0.000319420735859412, -0.000099406083911376, 0.000118496954373743, 0.000321150767788200, 0.000496840283261992, 0.000635932028877494, + 0.000731379294218185, 0.000779050717176191, 0.000777868837348859, 0.000729757323783321, 0.000639407293886249, 0.000513885517380605, + 0.000362113995846136, 0.000194254912976452, 0.000021049474537857, -0.000146857332295172, -0.000299553787970989, -0.000428434330673012, +-0.000526664670957786, -0.000589524295786869, -0.000614609034335983, -0.000601879769614216, -0.000553573238187010, -0.000473974817394057, +-0.000369078758947477, -0.000246159846025947, -0.000113285445089856, 0.000021201177134175, 0.000149189135268233, 0.000263277817392097, + 0.000357188814988566, 0.000426095215060785, 0.000466848645480576, 0.000478101121808084, 0.000460307676423158, 0.000415628506871784, + 0.000347728839468377, 0.000261502601122459, 0.000162731306278391, 0.000057705644340284, -0.000047160366530124, -0.000145719497401035, +-0.000232432591548855, -0.000302673831926362, -0.000352960919521696, -0.000381111227069818, -0.000386317254135060, -0.000369138410006413, +-0.000331410885812673, -0.000276095967153748, -0.000207064338646769, -0.000128840476454010, -0.000046322038661538, 0.000035506782941005, + 0.000111858949442223, 0.000178399161339807, 0.000231462231911356, 0.000268225243572190, 0.000286822428035563, 0.000286408115238973, + 0.000267145694421862, 0.000230168155042476, 0.000177457791675865, 0.000111715248667983, 0.000036181038541454, -0.000045562167590561, +-0.000129775708505696, -0.000212772068148448, -0.000291090671787090, -0.000361653837423190, -0.000421891912076977, -0.000469843487184555, +-0.000504199216087390, -0.000524326246532992, -0.000530244985591953, -0.000522587601943708, -0.000502508714800933, -0.000471602831913601, +-0.000431779497772114, -0.000385165398367582, -0.000333959777524947, -0.000280359514663211, -0.000226421731946560, -0.000174014477804677, +-0.000124740944604127, -0.000079866391614059, -0.000040362137084292, -0.000006825518135279, 0.000020462723568921, 0.000041517869163367, + 0.000056611414853593, 0.000066239730112684, 0.000071044828173283, 0.000071774979399342, 0.000069229653331724, 0.000064222904381994, + 0.000057525397104827, 0.000049840688763229, 0.000041775447258916, 0.000033835554436160, 0.000026407938202815, 0.000019768330546629, + 0.000014080571079703, 0.000009418988876053, 0.000008584643858565 ], + +800: # 576 taps +[ 0.000007812421315026, 0.000008939491599758, 0.000013730623424634, 0.000019922294330328, 0.000027648080357536, 0.000037033583075960, + 0.000048143861498476, 0.000060975611534136, 0.000075478577997365, 0.000091486545323856, 0.000108759338791293, 0.000126959389238696, + 0.000145652693573148, 0.000164310512299333, 0.000182322037177544, 0.000198999590569024, 0.000213614009898736, 0.000225404773677130, + 0.000233610866318186, 0.000237501949870309, 0.000236413452609260, 0.000229779689874953, 0.000217172712546861, 0.000198326327006374, + 0.000173169245803356, 0.000141846034715806, 0.000104732554871058, 0.000062442756567725, 0.000015827588463025, -0.000034041343381570, +-0.000085892061029301, -0.000138290085401831, -0.000189681915226441, -0.000238447248218383, -0.000282958234132596, -0.000321645514236948, +-0.000353063703221184, -0.000375960779331289, -0.000389338959490572, -0.000392510765617404, -0.000385145111580360, -0.000367301005108244, +-0.000339445383884735, -0.000302456824898826, -0.000257608154338197, -0.000206533574886596, -0.000151176196094362, -0.000093718946756075, +-0.000036501336346782, 0.000018073847262639, 0.000067643276268719, 0.000109986899426513, 0.000143132709269975, 0.000165454349492432, + 0.000175757544777044, 0.000173350787020117, 0.000158097036687462, 0.000130440616590328, 0.000091410233747216, 0.000042593436932362, +-0.000013915345466225, -0.000075592647414163, -0.000139587283955501, -0.000202842130917589, -0.000262229388149273, -0.000314696298480236, +-0.000357412309140960, -0.000387912413334777, -0.000404229097174175, -0.000405005829251745, -0.000389585053939050, -0.000358067160688509, +-0.000311334118688652, -0.000251037234855322, -0.000179546871425107, -0.000099865220473760, -0.000015504805729649, 0.000069662241126261, + 0.000151579558482804, 0.000226197017031803, 0.000289669081062027, 0.000338550686637535, 0.000369981142430884, 0.000381846581094768, + 0.000372912653196213, 0.000342918280693206, 0.000292625889758754, 0.000223822598023746, 0.000139271063990380, 0.000042610318846710, +-0.000061790478444653, -0.000169020516263524, -0.000273849135305279, -0.000370972701089329, -0.000455274194431906, -0.000522083581325246, +-0.000567425902673365, -0.000588244271105343, -0.000582585214321533, -0.000549736803809912, -0.000490309633189676, -0.000406255267998045, +-0.000300818490773199, -0.000178423294758992, -0.000044495794702522, 0.000094768978920233, 0.000232687846388931, 0.000362404482394644, + 0.000477224887882656, 0.000570957019366285, 0.000638237711550962, 0.000674829917199715, 0.000677874702997924, 0.000646082835685789, + 0.000579855135491155, 0.000481322426719260, 0.000354300348814663, 0.000204158180969291, 0.000037605243749942, -0.000137597792476766, +-0.000312990053930070, -0.000479809451013147, -0.000629420667419594, -0.000753751787384093, -0.000845717937205157, -0.000899610318070534, +-0.000911429428339170, -0.000879144467901314, -0.000802862554846735, -0.000684896491503848, -0.000529724099269400, -0.000343837417747543, +-0.000135485422990712, 0.000085680046384777, 0.000309041306180360, 0.000523514975880865, 0.000718092538346410, 0.000882395437488790, + 0.001007217243930078, 0.001085025011663583, 0.001110393602574907, 0.001080348246990215, 0.000994595533680270, 0.000855627164276610, + 0.000668686966519433, 0.000441598292681654, 0.000184456030731509, -0.000090806124488888, -0.000370954941565034, -0.000642080313861205, +-0.000890272488296022, -0.001102321495571061, -0.001266404272056495, -0.001372724600470066, -0.001414071700002426, -0.001386267150663963, +-0.001288473577308051, -0.001123345195626614, -0.000897007516317755, -0.000618861629989936, -0.000301217045862462, 0.000041233610152644, + 0.000392077118279518, 0.000733950102097711, 0.001049382185659160, 0.001321671816520764, 0.001535751682495805, 0.001678999613916214, + 0.001741952717366617, 0.001718885253761411, 0.001608217075415199, 0.001412726517149060, 0.001139550679396572, 0.000799966281315092, + 0.000408955257020119, -0.000015430291445883, -0.000452877901224665, -0.000881764419386712, -0.001280202174740378, -0.001627132056108478, +-0.001903410054728627, -0.002092832630669335, -0.002183047066222210, -0.002166297669634002, -0.002039964832379242, -0.001806863227100527, +-0.001475276496367705, -0.001058718218567234, -0.000575422161543602, -0.000047579005659538, 0.000499650640507829, 0.001039304486976568, + 0.001543915361236087, 0.001986875765177565, 0.002343803978935238, 0.002593843111794459, 0.002720826151891805, 0.002714244052422841, + 0.002569962094668674, 0.002290640442950059, 0.001885828071941918, 0.001371714610702517, 0.000770541414234860, 0.000109689735095918, +-0.000579518937674694, -0.001263260162418279, -0.001906817708604015, -0.002476294031416823, -0.002940338141748221, -0.003271805587759476, +-0.003449265353836316, -0.003458273739088719, -0.003292343903957135, -0.002953552492773730, -0.002452740833751403, -0.001809286792967657, +-0.001050443545598396, -0.000210263213371804, 0.000671856047428268, 0.001552940847463649, 0.002388454943494185, 0.003134462521134076, + 0.003749841173565993, 0.004198437225205720, 0.004451055565195895, 0.004487180085620033, 0.004296330509189801, 0.003878975870600702, + 0.003246943568337220, 0.002423285251527749, 0.001441585983790830, 0.000344729182561098, -0.000816843070689627, -0.001987309118898022, +-0.003107997569512837, -0.004120159055022873, -0.004967855525593480, -0.005600832763225632, -0.005977237320460713, -0.006066041703895594, +-0.005849050345318046, -0.005322373876566778, -0.004497280230451145, -0.003400356983323659, -0.002072948915619172, -0.000569867563016818, + 0.001042597072372037, 0.002689299061586182, 0.004289398058621653, 0.005759936423629122, 0.007019685172079656, 0.007993094199877881, + 0.008614171947670925, 0.008830116409127314, 0.008604524292277297, 0.007920018129346281, 0.006780151368661940, 0.005210478633443022, + 0.003258711582163722, 0.000993917994210585, -0.001495237701498300, -0.004103172616953404, -0.006710901704621617, -0.009190272912439044, +-0.011408767543032211, -0.013234683860549021, -0.014542500326924340, -0.015218201853231224, -0.015164348354310610, -0.014304669860257387, +-0.012587987008943695, -0.009991279084291772, -0.006521752749437525, -0.002217802857491734, 0.002851200149507507, 0.008586315129089878, + 0.014861597832277761, 0.021527754009268797, 0.028416755733892799, 0.035347257703852740, 0.042130616026199795, 0.048577285227121583, + 0.054503351348228368, 0.059736951256086870, 0.064124330411942668, 0.067535303652784234, 0.069867905879939018, 0.071052050289491700, + 0.071052050289491700, 0.069867905879939018, 0.067535303652784234, 0.064124330411942668, 0.059736951256086870, 0.054503351348228368, + 0.048577285227121583, 0.042130616026199795, 0.035347257703852740, 0.028416755733892799, 0.021527754009268797, 0.014861597832277761, + 0.008586315129089878, 0.002851200149507507, -0.002217802857491734, -0.006521752749437525, -0.009991279084291772, -0.012587987008943695, +-0.014304669860257387, -0.015164348354310610, -0.015218201853231224, -0.014542500326924340, -0.013234683860549021, -0.011408767543032211, +-0.009190272912439044, -0.006710901704621617, -0.004103172616953404, -0.001495237701498300, 0.000993917994210585, 0.003258711582163722, + 0.005210478633443022, 0.006780151368661940, 0.007920018129346281, 0.008604524292277297, 0.008830116409127314, 0.008614171947670925, + 0.007993094199877881, 0.007019685172079656, 0.005759936423629122, 0.004289398058621653, 0.002689299061586182, 0.001042597072372037, +-0.000569867563016818, -0.002072948915619172, -0.003400356983323659, -0.004497280230451145, -0.005322373876566778, -0.005849050345318046, +-0.006066041703895594, -0.005977237320460713, -0.005600832763225632, -0.004967855525593480, -0.004120159055022873, -0.003107997569512837, +-0.001987309118898022, -0.000816843070689627, 0.000344729182561098, 0.001441585983790830, 0.002423285251527749, 0.003246943568337220, + 0.003878975870600702, 0.004296330509189801, 0.004487180085620033, 0.004451055565195895, 0.004198437225205720, 0.003749841173565993, + 0.003134462521134076, 0.002388454943494185, 0.001552940847463649, 0.000671856047428268, -0.000210263213371804, -0.001050443545598396, +-0.001809286792967657, -0.002452740833751403, -0.002953552492773730, -0.003292343903957135, -0.003458273739088719, -0.003449265353836316, +-0.003271805587759476, -0.002940338141748221, -0.002476294031416823, -0.001906817708604015, -0.001263260162418279, -0.000579518937674694, + 0.000109689735095918, 0.000770541414234860, 0.001371714610702517, 0.001885828071941918, 0.002290640442950059, 0.002569962094668674, + 0.002714244052422841, 0.002720826151891805, 0.002593843111794459, 0.002343803978935238, 0.001986875765177565, 0.001543915361236087, + 0.001039304486976568, 0.000499650640507829, -0.000047579005659538, -0.000575422161543602, -0.001058718218567234, -0.001475276496367705, +-0.001806863227100527, -0.002039964832379242, -0.002166297669634002, -0.002183047066222210, -0.002092832630669335, -0.001903410054728627, +-0.001627132056108478, -0.001280202174740378, -0.000881764419386712, -0.000452877901224665, -0.000015430291445883, 0.000408955257020119, + 0.000799966281315092, 0.001139550679396572, 0.001412726517149060, 0.001608217075415199, 0.001718885253761411, 0.001741952717366617, + 0.001678999613916214, 0.001535751682495805, 0.001321671816520764, 0.001049382185659160, 0.000733950102097711, 0.000392077118279518, + 0.000041233610152644, -0.000301217045862462, -0.000618861629989936, -0.000897007516317755, -0.001123345195626614, -0.001288473577308051, +-0.001386267150663963, -0.001414071700002426, -0.001372724600470066, -0.001266404272056495, -0.001102321495571061, -0.000890272488296022, +-0.000642080313861205, -0.000370954941565034, -0.000090806124488888, 0.000184456030731509, 0.000441598292681654, 0.000668686966519433, + 0.000855627164276610, 0.000994595533680270, 0.001080348246990215, 0.001110393602574907, 0.001085025011663583, 0.001007217243930078, + 0.000882395437488790, 0.000718092538346410, 0.000523514975880865, 0.000309041306180360, 0.000085680046384777, -0.000135485422990712, +-0.000343837417747543, -0.000529724099269400, -0.000684896491503848, -0.000802862554846735, -0.000879144467901314, -0.000911429428339170, +-0.000899610318070534, -0.000845717937205157, -0.000753751787384093, -0.000629420667419594, -0.000479809451013147, -0.000312990053930070, +-0.000137597792476766, 0.000037605243749942, 0.000204158180969291, 0.000354300348814663, 0.000481322426719260, 0.000579855135491155, + 0.000646082835685789, 0.000677874702997924, 0.000674829917199715, 0.000638237711550962, 0.000570957019366285, 0.000477224887882656, + 0.000362404482394644, 0.000232687846388931, 0.000094768978920233, -0.000044495794702522, -0.000178423294758992, -0.000300818490773199, +-0.000406255267998045, -0.000490309633189676, -0.000549736803809912, -0.000582585214321533, -0.000588244271105343, -0.000567425902673365, +-0.000522083581325246, -0.000455274194431906, -0.000370972701089329, -0.000273849135305279, -0.000169020516263524, -0.000061790478444653, + 0.000042610318846710, 0.000139271063990380, 0.000223822598023746, 0.000292625889758754, 0.000342918280693206, 0.000372912653196213, + 0.000381846581094768, 0.000369981142430884, 0.000338550686637535, 0.000289669081062027, 0.000226197017031803, 0.000151579558482804, + 0.000069662241126261, -0.000015504805729649, -0.000099865220473760, -0.000179546871425107, -0.000251037234855322, -0.000311334118688652, +-0.000358067160688509, -0.000389585053939050, -0.000405005829251745, -0.000404229097174175, -0.000387912413334777, -0.000357412309140960, +-0.000314696298480236, -0.000262229388149273, -0.000202842130917589, -0.000139587283955501, -0.000075592647414163, -0.000013915345466225, + 0.000042593436932362, 0.000091410233747216, 0.000130440616590328, 0.000158097036687462, 0.000173350787020117, 0.000175757544777044, + 0.000165454349492432, 0.000143132709269975, 0.000109986899426513, 0.000067643276268719, 0.000018073847262639, -0.000036501336346782, +-0.000093718946756075, -0.000151176196094362, -0.000206533574886596, -0.000257608154338197, -0.000302456824898826, -0.000339445383884735, +-0.000367301005108244, -0.000385145111580360, -0.000392510765617404, -0.000389338959490572, -0.000375960779331289, -0.000353063703221184, +-0.000321645514236948, -0.000282958234132596, -0.000238447248218383, -0.000189681915226441, -0.000138290085401831, -0.000085892061029301, +-0.000034041343381570, 0.000015827588463025, 0.000062442756567725, 0.000104732554871058, 0.000141846034715806, 0.000173169245803356, + 0.000198326327006374, 0.000217172712546861, 0.000229779689874953, 0.000236413452609260, 0.000237501949870309, 0.000233610866318186, + 0.000225404773677130, 0.000213614009898736, 0.000198999590569024, 0.000182322037177544, 0.000164310512299333, 0.000145652693573148, + 0.000126959389238696, 0.000108759338791293, 0.000091486545323856, 0.000075478577997365, 0.000060975611534136, 0.000048143861498476, + 0.000037033583075960, 0.000027648080357536, 0.000019922294330328, 0.000013730623424634, 0.000008939491599758, 0.000007812421315026], + +400: # Shape 1.3, 770 taps +[ 0.000006365988874769, 0.000003794338487480, 0.000004876278267121, 0.000006131564967683, 0.000007569854655698, 0.000009199819153557, + 0.000011019782182642, 0.000013040101237056, 0.000015265995566942, 0.000017696391039940, 0.000020312791765427, 0.000023135950087517, + 0.000026132051354026, 0.000029299266022519, 0.000032613010658575, 0.000036050466832082, 0.000039588439426462, 0.000043195256918759, + 0.000046833693036583, 0.000050464843382682, 0.000054046630064787, 0.000057526210900478, 0.000060855931076401, 0.000063977534092402, + 0.000066834004766232, 0.000069367495918698, 0.000071512066722409, 0.000073206600728293, 0.000074384990732719, 0.000074985154083054, + 0.000074941358848070, 0.000074196098488127, 0.000072687036065854, 0.000070364017870395, 0.000067174711830892, 0.000063076338216280, + 0.000058031241355323, 0.000052010623717769, 0.000044992453647518, 0.000036966078084084, 0.000027928525061234, 0.000017889642287636, + 0.000006870052370296, -0.000005097430373119, -0.000017968467835226, -0.000031683467914261, -0.000046174009079182, -0.000061354520788796, +-0.000077131717460578, -0.000093396057473154, -0.000110030073586572, -0.000126901644954483, -0.000143873358110243, -0.000160793959726774, +-0.000177509258936783, -0.000193855013323854, -0.000209665383215103, -0.000224769517767120, -0.000238997508310048, -0.000252178063434561, +-0.000264145812850823, -0.000274737753978821, -0.000283800555901376, -0.000291188041391623, -0.000296767743750952, -0.000300418295008671, +-0.000302037282504393, -0.000301535929224389, -0.000298849141904324, -0.000293929053180242, -0.000286753991998278, -0.000277322604938755, +-0.000265661916507038, -0.000251820837747345, -0.000235878209412169, -0.000217935329921142, -0.000198122592158803, -0.000176593305324287, +-0.000153527761718214, -0.000129127285942292, -0.000103617728527825, -0.000077242169252941, -0.000050264484742360, -0.000022961141775614, + 0.000004376321843836, 0.000031449535456591, 0.000057950921452100, 0.000083573577884305, 0.000108007974814302, 0.000130953051335089, + 0.000152112926634605, 0.000171207503556438, 0.000187970143706098, 0.000202157681598790, 0.000213547707697876, 0.000221948537710715, + 0.000227196325735486, 0.000229163803316040, 0.000227757838181676, 0.000222926398781727, 0.000214655798042235, 0.000202977177552243, + 0.000187962083392261, 0.000169728357656893, 0.000148434669356957, 0.000124285090373991, 0.000097522588191792, 0.000068432637827430, + 0.000037334851940437, 0.000004586327504349, -0.000029428168903046, -0.000064294238370152, -0.000099577998322800, -0.000134825630330724, +-0.000169573965195115, -0.000203350785393334, -0.000235686105255552, -0.000266113076153881, -0.000294178911934326, -0.000319446762108148, +-0.000341506152453346, -0.000359974720676106, -0.000374508618833672, -0.000384803219892303, -0.000390603203520662, -0.000391702472886875, +-0.000387952968439801, -0.000379263512998934, -0.000365607622249019, -0.000347020514221526, -0.000323605716996244, -0.000295530392174600, +-0.000263029891857937, -0.000226402015568579, -0.000186009246146028, -0.000142271402525828, -0.000095666125277552, -0.000046719850734115, + 0.000003993583197103, 0.000055863130913344, 0.000108243891748510, 0.000160468312893279, 0.000211850586288707, 0.000261699269396572, + 0.000309322128759128, 0.000354039461924243, 0.000395189349018748, 0.000432140920160531, 0.000464299583558709, 0.000491120046640221, + 0.000512110607671310, 0.000526845446928034, 0.000534967981519684, 0.000536201343488612, 0.000530350597696912, 0.000517311013160068, + 0.000497068439035188, 0.000469705445435111, 0.000435399334882175, 0.000394425628337960, 0.000347153886365670, 0.000294048338105669, + 0.000235661140309264, 0.000172630559665850, 0.000105671355374271, 0.000035570791815593, -0.000036823410279226, -0.000110609450083053, +-0.000184845354264341, -0.000258557173028134, -0.000330754528114594, -0.000400439970264790, -0.000466625524492906, -0.000528342888003039, +-0.000584659866982658, -0.000634690947746067, -0.000677613119135274, -0.000712675673752122, -0.000739214923907549, -0.000756662495413132, +-0.000764558104223763, -0.000762555872583250, -0.000750434429795932, -0.000728100396603541, -0.000695595696462148, -0.000653097503960073, +-0.000600922257516729, -0.000539522021491309, -0.000469484695604341, -0.000391526816237233, -0.000306489974421006, -0.000215329834691466, +-0.000119109058134576, -0.000018982981184245, 0.000083810915235651, 0.000187970000243908, 0.000292139959571129, 0.000394933986966016, + 0.000494948142664746, 0.000590782205985295, 0.000681056090464857, 0.000764431197740454, 0.000839627050452169, 0.000905442225580665, + 0.000960769995583447, 0.001004618039369409, 0.001036121906273736, 0.001054562391583931, 0.001059376177251162, 0.001050169688173629, + 0.001026726061735900, 0.000989014843850052, 0.000937194466007411, 0.000871617237374737, 0.000792826925935815, 0.000701558421554040, + 0.000598730377991698, 0.000485439514972828, 0.000362948081805026, 0.000232673195056634, 0.000096169337651215, -0.000044886850392886, +-0.000188718302856941, -0.000333466707544108, -0.000477217660879214, -0.000618022612530573, -0.000753926550301084, -0.000882991568244702, +-0.001003325671471952, -0.001113106854174517, -0.001210611462788910, -0.001294237402501427, -0.001362530796885569, -0.001414206728098591, +-0.001448172776524704, -0.001463546100508022, -0.001459672417038066, -0.001436138251655395, -0.001392784297634421, -0.001329711615112041, +-0.001247288727402393, -0.001146150989584800, -0.001027200706007495, -0.000891599407168167, -0.000740760728594229, -0.000576335492878354, +-0.000400197621159931, -0.000214422239224392, -0.000021265147290331, 0.000176865113062629, 0.000377433546776307, 0.000577811529416548, + 0.000775307530646278, 0.000967203529925253, 0.001150788527481832, 0.001323396897145278, 0.001482443038707630, 0.001625459660097671, + 0.001750131670233381, 0.001854332615855901, 0.001936155690016456, 0.001993946486300650, 0.002026329220986863, 0.002032233882826800, + 0.002010916100718580, 0.001961977029179007, 0.001885375282542786, 0.001781438385022068, 0.001650865633292049, 0.001494730107495952, + 0.001314471906401099, 0.001111890114781271, 0.000889126269781853, 0.000648646430391355, 0.000393215009447102, 0.000125867561738431, +-0.000150124130738416, -0.000431287566747790, -0.000713991806927365, -0.000994489713483095, -0.001268965941124358, -0.001533583756312415, +-0.001784536663195940, -0.002018097583665120, -0.002230671541941067, -0.002418844734804261, -0.002579435657364371, -0.002709541359817244, +-0.002806584464746950, -0.002868353953987823, -0.002893045404115256, -0.002879294007683593, -0.002826205616681453, -0.002733379783299579, +-0.002600929724527880, -0.002429493528198460, -0.002220241603361130, -0.001974874872112559, -0.001695618645272131, -0.001385207220497454, +-0.001046863974406666, -0.000684272287342287, -0.000301542320981976, 0.000096831014522634, 0.000506013584333645, 0.000920885468043068, + 0.001336097138497067, 0.001746132300635148, 0.002145372272540894, 0.002528165649885475, 0.002888898066467300, 0.003222065542166644, + 0.003522346486582096, 0.003784675282650529, 0.004004312987512708, 0.004176917749158860, 0.004298610545483354, 0.004366038868235595, + 0.004376434088990949, 0.004327664995106265, 0.004218283602051056, 0.004047565581181025, 0.003815541611827637, 0.003523022274162837, + 0.003171612798693589, 0.002763720451889364, 0.002302551278585364, 0.001792098861036779, 0.001237122342212990, 0.000643116381836866, + 0.000016270569630469, -0.000636578845765101, -0.001308006968347628, -0.001990065317450888, -0.002674355367539247, -0.003352107995747014, +-0.004014269925046706, -0.004651594401548673, -0.005254737118938589, -0.005814354519325696, -0.006321205162109337, -0.006766251436254966, +-0.007140762118819473, -0.007436412979114623, -0.007645386018101693, -0.007760464454403432, -0.007775124017275518, -0.007683617984862557, +-0.007481056258606499, -0.007163476264301046, -0.006727906019989577, -0.006172417333684170, -0.005496169589494688, -0.004699442443010971, +-0.003783657807377434, -0.002751390001364686, -0.001606364515685684, -0.000353444465756940, 0.001001394411574224, 0.002451100972703334, + 0.003987597007902468, 0.005601837062643716, 0.007283878572786960, 0.009022962344277727, 0.010807601882596756, 0.012625681525580169, + 0.014464561665273282, 0.016311190660485246, 0.018152221825464991, 0.019974134681837181, 0.021763358913199277, 0.023506400111173612, + 0.025189965611159389, 0.026801089460580949, 0.028327254981021979, 0.029756513765852269, 0.031077599771363505, 0.032280037413131318, + 0.033354242302380432, 0.034291613871121174, 0.035084618575605728, 0.035726863022388727, 0.036213156095256299, 0.036539559528566301, + 0.036703426247133436, 0.036703426247133436, 0.036539559528566301, 0.036213156095256299, 0.035726863022388727, 0.035084618575605728, + 0.034291613871121174, 0.033354242302380432, 0.032280037413131318, 0.031077599771363505, 0.029756513765852269, 0.028327254981021979, + 0.026801089460580949, 0.025189965611159389, 0.023506400111173612, 0.021763358913199277, 0.019974134681837181, 0.018152221825464991, + 0.016311190660485246, 0.014464561665273282, 0.012625681525580169, 0.010807601882596756, 0.009022962344277727, 0.007283878572786960, + 0.005601837062643716, 0.003987597007902468, 0.002451100972703334, 0.001001394411574224, -0.000353444465756940, -0.001606364515685684, +-0.002751390001364686, -0.003783657807377434, -0.004699442443010971, -0.005496169589494688, -0.006172417333684170, -0.006727906019989577, +-0.007163476264301046, -0.007481056258606499, -0.007683617984862557, -0.007775124017275518, -0.007760464454403432, -0.007645386018101693, +-0.007436412979114623, -0.007140762118819473, -0.006766251436254966, -0.006321205162109337, -0.005814354519325696, -0.005254737118938589, +-0.004651594401548673, -0.004014269925046706, -0.003352107995747014, -0.002674355367539247, -0.001990065317450888, -0.001308006968347628, +-0.000636578845765101, 0.000016270569630469, 0.000643116381836866, 0.001237122342212990, 0.001792098861036779, 0.002302551278585364, + 0.002763720451889364, 0.003171612798693589, 0.003523022274162837, 0.003815541611827637, 0.004047565581181025, 0.004218283602051056, + 0.004327664995106265, 0.004376434088990949, 0.004366038868235595, 0.004298610545483354, 0.004176917749158860, 0.004004312987512708, + 0.003784675282650529, 0.003522346486582096, 0.003222065542166644, 0.002888898066467300, 0.002528165649885475, 0.002145372272540894, + 0.001746132300635148, 0.001336097138497067, 0.000920885468043068, 0.000506013584333645, 0.000096831014522634, -0.000301542320981976, +-0.000684272287342287, -0.001046863974406666, -0.001385207220497454, -0.001695618645272131, -0.001974874872112559, -0.002220241603361130, +-0.002429493528198460, -0.002600929724527880, -0.002733379783299579, -0.002826205616681453, -0.002879294007683593, -0.002893045404115256, +-0.002868353953987823, -0.002806584464746950, -0.002709541359817244, -0.002579435657364371, -0.002418844734804261, -0.002230671541941067, +-0.002018097583665120, -0.001784536663195940, -0.001533583756312415, -0.001268965941124358, -0.000994489713483095, -0.000713991806927365, +-0.000431287566747790, -0.000150124130738416, 0.000125867561738431, 0.000393215009447102, 0.000648646430391355, 0.000889126269781853, + 0.001111890114781271, 0.001314471906401099, 0.001494730107495952, 0.001650865633292049, 0.001781438385022068, 0.001885375282542786, + 0.001961977029179007, 0.002010916100718580, 0.002032233882826800, 0.002026329220986863, 0.001993946486300650, 0.001936155690016456, + 0.001854332615855901, 0.001750131670233381, 0.001625459660097671, 0.001482443038707630, 0.001323396897145278, 0.001150788527481832, + 0.000967203529925253, 0.000775307530646278, 0.000577811529416548, 0.000377433546776307, 0.000176865113062629, -0.000021265147290331, +-0.000214422239224392, -0.000400197621159931, -0.000576335492878354, -0.000740760728594229, -0.000891599407168167, -0.001027200706007495, +-0.001146150989584800, -0.001247288727402393, -0.001329711615112041, -0.001392784297634421, -0.001436138251655395, -0.001459672417038066, +-0.001463546100508022, -0.001448172776524704, -0.001414206728098591, -0.001362530796885569, -0.001294237402501427, -0.001210611462788910, +-0.001113106854174517, -0.001003325671471952, -0.000882991568244702, -0.000753926550301084, -0.000618022612530573, -0.000477217660879214, +-0.000333466707544108, -0.000188718302856941, -0.000044886850392886, 0.000096169337651215, 0.000232673195056634, 0.000362948081805026, + 0.000485439514972828, 0.000598730377991698, 0.000701558421554040, 0.000792826925935815, 0.000871617237374737, 0.000937194466007411, + 0.000989014843850052, 0.001026726061735900, 0.001050169688173629, 0.001059376177251162, 0.001054562391583931, 0.001036121906273736, + 0.001004618039369409, 0.000960769995583447, 0.000905442225580665, 0.000839627050452169, 0.000764431197740454, 0.000681056090464857, + 0.000590782205985295, 0.000494948142664746, 0.000394933986966016, 0.000292139959571129, 0.000187970000243908, 0.000083810915235651, +-0.000018982981184245, -0.000119109058134576, -0.000215329834691466, -0.000306489974421006, -0.000391526816237233, -0.000469484695604341, +-0.000539522021491309, -0.000600922257516729, -0.000653097503960073, -0.000695595696462148, -0.000728100396603541, -0.000750434429795932, +-0.000762555872583250, -0.000764558104223763, -0.000756662495413132, -0.000739214923907549, -0.000712675673752122, -0.000677613119135274, +-0.000634690947746067, -0.000584659866982658, -0.000528342888003039, -0.000466625524492906, -0.000400439970264790, -0.000330754528114594, +-0.000258557173028134, -0.000184845354264341, -0.000110609450083053, -0.000036823410279226, 0.000035570791815593, 0.000105671355374271, + 0.000172630559665850, 0.000235661140309264, 0.000294048338105669, 0.000347153886365670, 0.000394425628337960, 0.000435399334882175, + 0.000469705445435111, 0.000497068439035188, 0.000517311013160068, 0.000530350597696912, 0.000536201343488612, 0.000534967981519684, + 0.000526845446928034, 0.000512110607671310, 0.000491120046640221, 0.000464299583558709, 0.000432140920160531, 0.000395189349018748, + 0.000354039461924243, 0.000309322128759128, 0.000261699269396572, 0.000211850586288707, 0.000160468312893279, 0.000108243891748510, + 0.000055863130913344, 0.000003993583197103, -0.000046719850734115, -0.000095666125277552, -0.000142271402525828, -0.000186009246146028, +-0.000226402015568579, -0.000263029891857937, -0.000295530392174600, -0.000323605716996244, -0.000347020514221526, -0.000365607622249019, +-0.000379263512998934, -0.000387952968439801, -0.000391702472886875, -0.000390603203520662, -0.000384803219892303, -0.000374508618833672, +-0.000359974720676106, -0.000341506152453346, -0.000319446762108148, -0.000294178911934326, -0.000266113076153881, -0.000235686105255552, +-0.000203350785393334, -0.000169573965195115, -0.000134825630330724, -0.000099577998322800, -0.000064294238370152, -0.000029428168903046, + 0.000004586327504349, 0.000037334851940437, 0.000068432637827430, 0.000097522588191792, 0.000124285090373991, 0.000148434669356957, + 0.000169728357656893, 0.000187962083392261, 0.000202977177552243, 0.000214655798042235, 0.000222926398781727, 0.000227757838181676, + 0.000229163803316040, 0.000227196325735486, 0.000221948537710715, 0.000213547707697876, 0.000202157681598790, 0.000187970143706098, + 0.000171207503556438, 0.000152112926634605, 0.000130953051335089, 0.000108007974814302, 0.000083573577884305, 0.000057950921452100, + 0.000031449535456591, 0.000004376321843836, -0.000022961141775614, -0.000050264484742360, -0.000077242169252941, -0.000103617728527825, +-0.000129127285942292, -0.000153527761718214, -0.000176593305324287, -0.000198122592158803, -0.000217935329921142, -0.000235878209412169, +-0.000251820837747345, -0.000265661916507038, -0.000277322604938755, -0.000286753991998278, -0.000293929053180242, -0.000298849141904324, +-0.000301535929224389, -0.000302037282504393, -0.000300418295008671, -0.000296767743750952, -0.000291188041391623, -0.000283800555901376, +-0.000274737753978821, -0.000264145812850823, -0.000252178063434561, -0.000238997508310048, -0.000224769517767120, -0.000209665383215103, +-0.000193855013323854, -0.000177509258936783, -0.000160793959726774, -0.000143873358110243, -0.000126901644954483, -0.000110030073586572, +-0.000093396057473154, -0.000077131717460578, -0.000061354520788796, -0.000046174009079182, -0.000031683467914261, -0.000017968467835226, +-0.000005097430373119, 0.000006870052370296, 0.000017889642287636, 0.000027928525061234, 0.000036966078084084, 0.000044992453647518, + 0.000052010623717769, 0.000058031241355323, 0.000063076338216280, 0.000067174711830892, 0.000070364017870395, 0.000072687036065854, + 0.000074196098488127, 0.000074941358848070, 0.000074985154083054, 0.000074384990732719, 0.000073206600728293, 0.000071512066722409, + 0.000069367495918698, 0.000066834004766232, 0.000063977534092402, 0.000060855931076401, 0.000057526210900478, 0.000054046630064787, + 0.000050464843382682, 0.000046833693036583, 0.000043195256918759, 0.000039588439426462, 0.000036050466832082, 0.000032613010658575, + 0.000029299266022519, 0.000026132051354026, 0.000023135950087517, 0.000020312791765427, 0.000017696391039940, 0.000015265995566942, + 0.000013040101237056, 0.000011019782182642, 0.000009199819153557, 0.000007569854655698, 0.000006131564967683, 0.000004876278267121, + 0.000003794338487480, 0.000006365988874769], + +} diff --git a/freedv.c b/freedv.c new file mode 100644 index 0000000..23d3cf7 --- /dev/null +++ b/freedv.c @@ -0,0 +1,707 @@ +#include +#include +#include +#include // Use native C99 complex type for fftw3 +#include + +#include "quisk.h" +#include "freedv.h" +#include + +int DEBUG; + +#define MAX_RECEIVERS 2 + +typedef struct { // from comp.h + float real; + float imag; +} COMP; + +struct freedv; // from freedv_api.h +typedef void (*freedv_callback_rx)(void *, char); +typedef char (*freedv_callback_tx)(void *); +/* Protocol bits are packed MSB-first */ +/* Called when a frame containing protocol data is decoded */ +typedef void (*freedv_callback_protorx)(void *, char *); +/* Called when a frame containing protocol data is to be sent */ +typedef void (*freedv_callback_prototx)(void *, char *); +/* Data packet callbacks */ +/* Called when a packet has been received */ +typedef void (*freedv_callback_datarx)(void *, unsigned char *packet, size_t size); +/* Called when a new packet can be send */ +typedef void (*freedv_callback_datatx)(void *, unsigned char *packet, size_t *size); + +/* advanced freedv open options required by some modes */ +struct freedv_advanced { // from freedv_api.h + int interleave_frames; +}; + +#ifdef MS_WINDOWS +#include +HMODULE hLib; +#define GET_HANDLE1 hLib = LoadLibrary("libcodec2.dll") +#define GET_HANDLE2 hLib = LoadLibrary(".\\freedvpkg\\libcodec2.dll") +#define GET_HANDLE3 hLib = LoadLibrary(".\\freedvpkg\\libcodec2_32.dll") +#define GET_HANDLE4 hLib = LoadLibrary(".\\freedvpkg\\libcodec2_64.dll") +#define GET_ADDR(name) (void *)GetProcAddress(hLib, name) +#define CLOSE_LIB FreeLibrary(hLib) +#else +#include +void * hLib; +#define GET_HANDLE1 hLib = dlopen("libcodec2.so", RTLD_LAZY) +#define GET_HANDLE2 hLib = dlopen("./freedvpkg/libcodec2.so", RTLD_LAZY) +#define GET_HANDLE3 hLib = dlopen("./freedvpkg/libcodec2_32.so", RTLD_LAZY) +#define GET_HANDLE4 hLib = dlopen("./freedvpkg/libcodec2_64.so", RTLD_LAZY) +#define GET_ADDR(name) dlsym(hLib, name) +#define CLOSE_LIB dlclose(hLib) +#endif + +static int requested_mode = -1; // requested mode +int freedv_current_mode = -1; // the current running mode +static int quisk_freedv_squelch; +static int interleave_frames = 1; +static int freedv_version = -1; +static int quisk_set_tx_bpf = 1; +static int handle_index = -1; +// need access to these here and in quisk.c +int n_speech_sample_rate = 8000; +int n_modem_sample_rate = 8000; + +#define SPEECH_BUF_SIZE 12000 // speech buffer size increased from 3000 +static struct _rx_channel{ + struct freedv * hFreedv; + COMP * demod_in; + int rxdata_index; + short speech_out[SPEECH_BUF_SIZE]; // output buffer + int speech_available; // number of samples in output buffer + int playing; // are we currently returning speech samples? +} rx_channel[MAX_RECEIVERS] = { 0 }; // safe initialisation to 0 + +// freedv_version is the library version number, or +// -1 no library was found +// -2 a library was found, but freedv_get_version is missing + +// FreeDV API functions: +// open, close +struct freedv * (*freedv_open)(int mode); +struct freedv * (*freedv_open_advanced)(int mode, struct freedv_advanced *adv); +void (*freedv_close)(struct freedv *freedv); +// Transmit +void (*freedv_tx)(struct freedv *freedv, short *, short *); +void (*freedv_comptx)(struct freedv *freedv, COMP *, short *); +// Receive +int (*freedv_nin)(struct freedv *freedv); +int (*freedv_rx)(struct freedv *freedv, short *, short demod_in[]); +int (*freedv_floatrx)(struct freedv *freedv, short *, float demod_in[]); +int (*freedv_comprx)(struct freedv *freedv, short *, COMP demod_in[]); +// Set parameters +void (*freedv_set_callback_txt) (struct freedv *freedv, freedv_callback_rx rx, freedv_callback_tx tx, void *callback_state); +void (*freedv_set_test_frames) (struct freedv *freedv, int test_frames); +void (*freedv_set_smooth_symbols) (struct freedv *freedv, int smooth_symbols); +void (*freedv_set_squelch_en) (struct freedv *freedv, int squelch_en); +void (*freedv_set_snr_squelch_thresh) (struct freedv *freedv, float snr_squelch_thresh); +void (*freedv_set_tx_bpf) (struct freedv *freedv, int val); +// Get parameters +int (*freedv_get_version)(void); +void (*freedv_get_modem_stats)(struct freedv *freedv, int *sync, float *snr_est); +int (*freedv_get_test_frames) (struct freedv *freedv); +int (*freedv_get_n_speech_samples) (struct freedv *freedv); +int (*freedv_get_n_max_modem_samples) (struct freedv *freedv); +int (*freedv_get_n_nom_modem_samples) (struct freedv *freedv); +int (*freedv_get_total_bits) (struct freedv *freedv); +int (*freedv_get_total_bit_errors) (struct freedv *freedv); +int (*freedv_get_modem_sample_rate) (struct freedv *freedv); +int (*freedv_get_speech_sample_rate) (struct freedv *freedv); +// +int (*freedv_get_sync) (struct freedv *freedv); +void (*freedv_set_callback_protocol) (struct freedv *freedv, freedv_callback_protorx rx, freedv_callback_prototx tx, void *callback_state); +void (*freedv_set_callback_data) (struct freedv *freedv, freedv_callback_datarx datarx, freedv_callback_datatx datatx, void *callback_state); + +// +// checkAvxSupport +// +// This function was copied from freedv, file fdmdv2_main.cpp and tweaked for use here +// +// Tests the underlying platform for AVX support. 2020 needs AVX support to run +// in real-time, and old processors do not offer AVX support +// + +#if defined(__x86_64__) || defined(_M_X64) || defined(__i386) || defined(_M_IX86) +#include +static int checkAvxSupport(void) +{ + + int isAvxPresent = 0; + uint32_t eax, ebx, ecx, edx; + eax = ebx = ecx = edx = 0; + __cpuid(1, eax, ebx, ecx, edx); + + if (ecx & (1<<27) && ecx & (1<<28)) { + // CPU supports XSAVE and AVX + uint32_t xcr0, xcr0_high; + asm("xgetbv" : "=a" (xcr0), "=d" (xcr0_high) : "c" (0)); + isAvxPresent = (xcr0 & 6) == 6; // AVX state saving enabled? + } + return isAvxPresent; +} +#else +static int checkAvxSupport(void) +{ + return 0; +} +#endif + +/* Called when a new packet can be sent */ +void my_datatx(void *callback_state, unsigned char *packet, size_t *size) { + *size = 0; +} + +static void GetAddrs(void) +{ + handle_index = 1; + GET_HANDLE1; + if ( ! hLib) { + handle_index = 2; + GET_HANDLE2; + if ( ! hLib) { + handle_index = 3; + GET_HANDLE3; + if ( ! hLib) { + handle_index = 4; + GET_HANDLE4; + } + } + } + if (hLib) { + freedv_get_version = GET_ADDR("freedv_get_version"); + if (freedv_get_version != NULL) { + freedv_version = freedv_get_version(); + if (DEBUG) + printf("FreeDV codec2 library %d found, version %d\n", handle_index, freedv_version); + } + else { + freedv_version = -2; + if (DEBUG) + printf("FreeDV codec2 library %d found, but no freedv_get_version() !!!\n", handle_index); + CLOSE_LIB; + return; + } + } + else { + handle_index = -1; + if (DEBUG) + printf("Could not find the FreeDV codec2 shared library\n"); + return; + } + +// open, close + freedv_open = GET_ADDR("freedv_open"); + freedv_open_advanced = GET_ADDR("freedv_open_advanced"); + freedv_close = GET_ADDR("freedv_close"); +// Transmit + freedv_tx = GET_ADDR("freedv_tx"); + freedv_comptx = GET_ADDR("freedv_comptx"); +// Receive + freedv_nin = GET_ADDR("freedv_nin"); + freedv_rx = GET_ADDR("freedv_rx"); + freedv_floatrx = GET_ADDR("freedv_floatrx"); + freedv_comprx = GET_ADDR("freedv_comprx"); +// Set parameters + freedv_set_callback_txt = GET_ADDR("freedv_set_callback_txt"); + freedv_set_callback_protocol = GET_ADDR("freedv_set_callback_protocol"); + freedv_set_callback_data = GET_ADDR("freedv_set_callback_data"); + freedv_set_test_frames = GET_ADDR("freedv_set_test_frames"); + freedv_set_smooth_symbols = GET_ADDR("freedv_set_smooth_symbols"); + freedv_set_squelch_en = GET_ADDR("freedv_set_squelch_en"); + freedv_set_snr_squelch_thresh = GET_ADDR("freedv_set_snr_squelch_thresh"); + freedv_set_tx_bpf = GET_ADDR("freedv_set_tx_bpf"); +// Get parameters + freedv_get_modem_stats = GET_ADDR("freedv_get_modem_stats"); + freedv_get_test_frames = GET_ADDR("freedv_get_test_frames"); + freedv_get_n_speech_samples = GET_ADDR("freedv_get_n_speech_samples"); + freedv_get_n_max_modem_samples = GET_ADDR("freedv_get_n_max_modem_samples"); + freedv_get_n_nom_modem_samples = GET_ADDR("freedv_get_n_nom_modem_samples"); + freedv_get_total_bits = GET_ADDR("freedv_get_total_bits"); + freedv_get_total_bit_errors = GET_ADDR("freedv_get_total_bit_errors"); + freedv_get_sync = GET_ADDR("freedv_get_sync"); // requires version 11 + if( freedv_version >= 11) + freedv_get_modem_sample_rate = GET_ADDR("freedv_get_modem_sample_rate"); // requires version 11 or later + if (freedv_version >= 12) + freedv_get_speech_sample_rate = GET_ADDR("freedv_get_speech_sample_rate"); // requires version 12 + return; +} + +static int quisk_freedv_rx(complex double * cSamples, double * dsamples, int count, int bank) // Called from the sound thread. +{ // Input digital modulation is cSamples; decoded voice is dsamples. Each "bank" is a stream of audio. + // caller will have decimated input audio down to n_modem_sample_rate and reduced block size to match reduction + // output data will be returned at n_speech_sample_rate with corresponding block sizes + int i, nout, need, have, sync; + int n_speech_samples; + complex double cx; + double scale = (double)CLIP32 / CLIP16; // convert 32 bits to 16 bits + struct freedv * hF; + struct _rx_channel * pCh; + int nCountOut = 1; // In case we have a higher (or lower) value for o/p count + + if (cSamples == NULL) { // shutdown + for (i = 0; i < MAX_RECEIVERS; i++) { + if (rx_channel[i].demod_in) { + free(rx_channel[i].demod_in); + rx_channel[i].demod_in = NULL; + } + } + return 0; + } + + if (bank < 0 || bank >= MAX_RECEIVERS) + return 0; + hF = rx_channel[bank].hFreedv; + if ( ! hF) + return 0; + pCh = rx_channel + bank; + n_speech_samples = freedv_get_n_speech_samples(hF); + // if (say) input modem rate is the 2020 mode rate of 8000 sa/sec + // and output rate is the corresponding 16000 sa/sec then for an input + // block size of N, outbut 2N samples block size. + // Apply a multiplier/divisor to 'count' for the output samples count + if(n_speech_sample_rate >= n_modem_sample_rate ) { + int nRatio = n_speech_sample_rate / n_modem_sample_rate; + if( nRatio >= 1 && nRatio <= 6) + nCountOut = nRatio * count; + } + else { + int nRatio = n_modem_sample_rate / n_speech_sample_rate; + if( nRatio >= 1 && nRatio <= 6) + nCountOut = count / nRatio; + } + nout = 0; + need = freedv_nin(hF); + for (i = 0; i < count; i++) { + cx = cRxFilterOut(cSamples[i], bank, 0); + if (rxMode == FDV_L) // lower sideband + cx = conj(cx); +#if 1 // Try it the other way and remove scaling factor + pCh->demod_in[pCh->rxdata_index].real = creal(cx); + pCh->demod_in[pCh->rxdata_index].imag = cimag(cx); +#else + pCh->demod_in[pCh->rxdata_index].real = (creal(cx) - cimag(cx)); + pCh->demod_in[pCh->rxdata_index].imag = 0; +#endif + pCh->rxdata_index++; + if (pCh->rxdata_index >= need) { + if (pCh->speech_available + n_speech_samples < SPEECH_BUF_SIZE) { // check for buffer space + have = freedv_comprx(hF, pCh->speech_out + pCh->speech_available, pCh->demod_in); + if (freedv_version > 10) + sync = freedv_get_sync(hF); + else + freedv_get_modem_stats(hF, &sync, NULL); + if (freedv_current_mode == 0) { // mode 1600 + if (sync) // throw away speech if not in sync + pCh->speech_available += have; + } + else if (pCh->speech_available < SPEECH_BUF_SIZE * 2 / 3) { + pCh->speech_available += have; // keep speech if there is space + } + else { + if (DEBUG) printf("Close to maximum in speech output buffer\n"); + } + } + else { // no space in buffer + if (DEBUG) printf("Overflow in speech output buffer\n"); + } + pCh->rxdata_index = 0; + need = freedv_nin(hF); + } + } + if ( ! pCh->playing) { + if (pCh->speech_available >= 2 * n_speech_samples) { + pCh->playing = 1; + } + else { // return zero samples + for (i = 0; i < nCountOut; i++) + dsamples[i] = 0; + //if (DEBUG) printf("Rx buffer playing %d available %d\n", pCh->playing, pCh->speech_available); + return nCountOut; + } + } + for (nout = 0; nout < pCh->speech_available && nout < nCountOut; nout++) + dsamples[nout] = pCh->speech_out[nout] * scale * 0.7; + if (nout) { + pCh->speech_available -= nout; + memmove(pCh->speech_out, pCh->speech_out + nout, (pCh->speech_available) * sizeof(short)); + } + if ( ! pCh->speech_available) { + pCh->playing = 0; + while (nout < nCountOut) + dsamples[nout++] = 0; + } + //if (DEBUG) printf("Rx buffer playing %d available %d\n", pCh->playing, pCh->speech_available); + return nout; +} + +static int quisk_freedv_tx(complex double * cSamples, double * dsamples, int count) // Called from the sound thread. +{ // Input voice samples are dsamples; output digital modulation is cSamples. + int i, nout; + int n_speech_samples; + int n_nom_modem_samples; + static COMP * mod_out = NULL; + static short * speech_in = NULL; + static int speech_index=0, mod_index=0; + // g8kbb - add for 800XA + static short *real_mod_out = NULL; + + if (dsamples == NULL) { // shutdown + if (mod_out) + free(mod_out); + mod_out = NULL; + if (real_mod_out) + free(real_mod_out); + real_mod_out = NULL; + if (speech_in) + free(speech_in); + speech_in = NULL; + return 0; + } + if ( ! rx_channel[0].hFreedv) + return 0; + n_speech_samples = freedv_get_n_speech_samples(rx_channel[0].hFreedv); + n_nom_modem_samples = freedv_get_n_nom_modem_samples(rx_channel[0].hFreedv); + // find if need to output at faster rate + int nRatio = n_modem_sample_rate / n_speech_sample_rate; + if( nRatio < 1 || nRatio > 6 ) + nRatio = 1; + if (mod_out == NULL) { // initialize + mod_out = (COMP *)malloc(sizeof(COMP) * n_nom_modem_samples); + memset(mod_out, 0, sizeof(COMP) * n_nom_modem_samples); + speech_in = (short*)malloc(sizeof(short) * n_speech_samples); + speech_index=0; + mod_index=0; + real_mod_out = (short *)malloc(sizeof(short) * n_nom_modem_samples); + memset(real_mod_out, 0, sizeof(short) * n_nom_modem_samples); + //if( DEBUG ) printf("Initialise tx buffer size = %d, i/p sample size = %d\n", n_nom_modem_samples, n_speech_samples ); + } + nout = 0; + for (i = 0; i < count; i++) { + speech_in[speech_index++] = (short)dsamples[i]; + if (speech_index >= n_speech_samples) { + // Calculate a new block, but first write out the rest of the old block + // 800xa uses real not complex + if( freedv_current_mode == FREEDV_MODE_800XA ) { + for ( ; mod_index < n_nom_modem_samples; mod_index++) + cSamples[nout++] = real_mod_out[mod_index]; + freedv_tx(rx_channel[0].hFreedv, real_mod_out, speech_in); + } + else { + for ( ; mod_index < n_nom_modem_samples; mod_index++) + cSamples[nout++] = mod_out[mod_index].real + I * mod_out[mod_index].imag; + freedv_comptx(rx_channel[0].hFreedv, mod_out, speech_in); + } + //if( DEBUG ) printf("Block processed, speech index=%d, n_speech_samples=%d, count=%d\n", speech_index, n_speech_samples, count ); + mod_index = 0; + speech_index = 0; + } + else { // write out samples + for( int j=0; j= TX_MSG_SIZE) + index = 0; + if ( ! c) { + index = 0; + c = quisk_tx_msg[index++]; + } + return c; +} + +#define RX_MSG_SIZE 80 +static char quisk_rx_msg[RX_MSG_SIZE + 1]; + +static void put_next_rx_char(void * callback_state, char ch) +{ + if (ch == '\n' || ch == '\r') + ch = ' '; + if (ch < 32 || ch > 126) // printable characters + return; + if (strlen(quisk_rx_msg) < RX_MSG_SIZE) + strncat(quisk_rx_msg, &ch, 1); +} + +PyObject * quisk_freedv_get_rx_char(PyObject * self, PyObject * args) // Called from the GUI thread. +{ + PyObject * txt; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + txt = PyString_FromString(quisk_rx_msg); + quisk_rx_msg[0] = 0; + return txt; +} + +static void CloseFreedv(void) // Called from the GUI thread or sound thread +{ + int i; + + for (i = 0; i < MAX_RECEIVERS; i++) { + if (rx_channel[i].hFreedv) { + freedv_close(rx_channel[i].hFreedv); + rx_channel[i].hFreedv = NULL; + } +#if 0 // g8kbb - unnecessary - quisk_freedv_rx() will do it + if (rx_channel[i].demod_in) { + free(rx_channel[i].demod_in); + rx_channel[i].demod_in = NULL; + } +#endif + } + quisk_freedv_rx(NULL, NULL, 0, 0); + quisk_freedv_tx(NULL, NULL, 0); + freedv_current_mode = -1; +} + +static int OpenFreedv(void) // Called from the GUI thread or sound thread +{ + int i, n_max_modem_samples; + struct freedv * hF; + + if ( ! hLib) + GetAddrs(); // Get the entry points for funtions in the codec2 library + if (DEBUG) { + switch (handle_index) { + case 1: + printf("freedv_open: system codec2 library found, version %d\n", freedv_version); + break; + case 2: + printf("freedv_open: codec2 library freedvpkg/libcodec2.dll|so found, version %d\n", freedv_version); + break; + case 3: + case 4: + printf("freedv_open: codec2 library freedvpkg/libcodec2_32|64.dll|so found, version %d\n", freedv_version); + break; + default: + printf("freedv_open: Could not find the FreeDV codec2 shared library\n"); + break; + } + } + if (freedv_version < 10) { + CloseFreedv(); + requested_mode = -1; + if (DEBUG) + printf("freedv_open: Failure because version is less than 10\n"); + return 0; // failure + } + if (requested_mode == FREEDV_MODE_2020) { + if (checkAvxSupport() == 0) { + CloseFreedv(); + requested_mode = -1; + if (DEBUG) + printf("freedv_open: Failure because mode 2020 requires Avx support\n"); + return 0; // failure + } + if (handle_index >= 3) { // Quisk provided codec2 + CloseFreedv(); + requested_mode = -1; + if (DEBUG) + printf("freedv_open: Failure because mode 2020 requires a system installation of codec2\n"); + return 0; // failure + } + } + if (requested_mode == FREEDV_MODE_700E) { + if (handle_index >= 3) { // Quisk provided codec2 + CloseFreedv(); + requested_mode = -1; + if (DEBUG) + printf("freedv_open: Failure because mode 700E requires a system installation of codec2\n"); + return 0; // failure + } + } + + if ((requested_mode == FREEDV_MODE_700D || requested_mode == FREEDV_MODE_700E) && freedv_open_advanced) { + struct freedv_advanced adv; + adv.interleave_frames = interleave_frames; + hF = freedv_open_advanced(requested_mode, &adv); + } + else { + hF = freedv_open(requested_mode); + } + if (hF == NULL) { + CloseFreedv(); + requested_mode = -1; + return 0; // failure + } + rx_channel[0].hFreedv = hF; + quisk_dvoice_freedv(&quisk_freedv_rx, &quisk_freedv_tx); + if (quisk_tx_msg[0]) + freedv_set_callback_txt(hF, &put_next_rx_char, &get_next_tx_char, NULL); + else + freedv_set_callback_txt(hF, &put_next_rx_char, NULL, NULL); + if (freedv_set_callback_protocol) + freedv_set_callback_protocol(hF, NULL, NULL, NULL); + if (freedv_set_callback_data) + freedv_set_callback_data(hF, NULL, my_datatx, NULL); + freedv_set_squelch_en(hF, quisk_freedv_squelch); + if (freedv_set_tx_bpf) + freedv_set_tx_bpf(hF, quisk_set_tx_bpf); + n_max_modem_samples = freedv_get_n_max_modem_samples(hF); + n_speech_sample_rate = 8000; + if (freedv_version >= 12 && freedv_get_speech_sample_rate) + n_speech_sample_rate = freedv_get_speech_sample_rate(hF); + n_modem_sample_rate = 8000; + if (freedv_version >= 11 && freedv_get_modem_sample_rate) + n_modem_sample_rate = freedv_get_modem_sample_rate(hF); + for (i = 0; i < MAX_RECEIVERS; i++) { + rx_channel[i].rxdata_index = 0; + rx_channel[i].speech_available = 0; + rx_channel[i].playing = 0; + if (rx_channel[i].demod_in) + free(rx_channel[i].demod_in); + rx_channel[i].demod_in = (COMP *)malloc(sizeof(COMP) * n_max_modem_samples); + if( rx_channel[i].demod_in == NULL) { + CloseFreedv(); + requested_mode = -1; + return 0; // failure + } + if (i > 0) { + rx_channel[i].hFreedv = freedv_open(requested_mode); + if (rx_channel[i].hFreedv) + freedv_set_squelch_en(rx_channel[i].hFreedv, quisk_freedv_squelch); + } + } + if (DEBUG) printf("n_nom_modem_samples %d\n", freedv_get_n_nom_modem_samples(rx_channel[0].hFreedv)); + if (DEBUG) printf("n_speech_samples %d\n", freedv_get_n_speech_samples(rx_channel[0].hFreedv)); + if (DEBUG) printf("n_max_modem_samples %d\n", n_max_modem_samples); + if (DEBUG) { + if (freedv_version >= 11) printf("modem_sample_rate %d\n", n_modem_sample_rate); + if (freedv_version >= 12) printf("speech_sample_rate %d\n", n_speech_sample_rate); + } + freedv_current_mode = requested_mode; + return 1; // success +} + +void quisk_check_freedv_mode(void) +{ // see if we need to change the mode + if (requested_mode == freedv_current_mode) + return; + if (DEBUG) printf("Change in mode to %d\n", requested_mode); + CloseFreedv(); + if (requested_mode >= 0) + OpenFreedv(); + else + requested_mode = -1; +} + +PyObject * quisk_freedv_open(PyObject * self, PyObject * args) // Called from the GUI thread before freedv is open +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + return PyInt_FromLong(OpenFreedv()); +} + +PyObject * quisk_freedv_close(PyObject * self, PyObject * args) // Called from the GUI thread. +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + requested_mode = -1; // request close + Py_INCREF (Py_None); + return Py_None; +} + +PyObject * quisk_freedv_set_options(PyObject * self, PyObject * args, PyObject * keywds) // Called from the GUI thread. +{ // Call with keyword arguments ONLY to change parameters. Call before quisk_freedv_open() to set an initial mode. + int mode=-1; // Call again to change the mode. + int bpf=-1; + char * ptMsg=NULL; + static char * kwlist[] = {"mode", "tx_msg", "DEBUG", "squelch", "interleave_frames", "set_tx_bpf", NULL} ; + + if (!PyArg_ParseTupleAndKeywords (args, keywds, "|isiiii", kwlist, &mode, &ptMsg, &DEBUG, &quisk_freedv_squelch, &interleave_frames, &bpf)) + return NULL; + if (ptMsg) + strMcpy(quisk_tx_msg, ptMsg, TX_MSG_SIZE); + if (bpf != -1) { + quisk_set_tx_bpf = bpf; + if (freedv_set_tx_bpf && rx_channel[0].hFreedv) + freedv_set_tx_bpf(rx_channel[0].hFreedv, quisk_set_tx_bpf); + } + if (mode == -1) { // mode was not entered + ; + } + else if (mode == FREEDV_MODE_700E && freedv_version >= 14) { + requested_mode = mode; + } + else if (mode == FREEDV_MODE_2020) { + if (checkAvxSupport() && handle_index <= 2 && freedv_version >= 13) + requested_mode = mode; + } + else if (freedv_version == 10) { + if (mode == 0) + requested_mode = mode; + } + else if (freedv_version == 11) { + if (mode <= 2) + requested_mode = mode; + } + else { + requested_mode = mode; + } + return PyInt_FromLong(requested_mode); // Return the mode +} + +PyObject * quisk_freedv_get_snr(PyObject * self, PyObject * args) // Called from the GUI thread. +{ + float snr_est = 0.0; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + if (rx_channel[0].hFreedv) + freedv_get_modem_stats(rx_channel[0].hFreedv, NULL, &snr_est); + return PyFloat_FromDouble(snr_est); +} + +PyObject * quisk_freedv_get_version(PyObject * self, PyObject * args) // Called from the GUI thread. +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + if ( ! hLib) + GetAddrs(); // Get the entry points for funtions in the codec2 library + return PyInt_FromLong(freedv_version); +} + +PyObject * quisk_freedv_set_squelch_en(PyObject * self, PyObject * args) // Called from the GUI thread. +{ + int i,state; + + if (!PyArg_ParseTuple (args, "i", &state)) + return NULL; + quisk_freedv_squelch = state; + for (i = 0; i < MAX_RECEIVERS; i++) { + if( rx_channel[i].hFreedv ) + freedv_set_squelch_en(rx_channel[i].hFreedv, state); + } + return PyInt_FromLong(state); +} + + diff --git a/freedv.h b/freedv.h new file mode 100644 index 0000000..d1932f9 --- /dev/null +++ b/freedv.h @@ -0,0 +1,12 @@ +extern int freedv_current_mode; // the current FreeDV running mode + +#define FREEDV_MODE_1600 0 +#define FREEDV_MODE_700 1 +#define FREEDV_MODE_700B 2 +#define FREEDV_MODE_2400A 3 +#define FREEDV_MODE_2400B 4 +#define FREEDV_MODE_800XA 5 +#define FREEDV_MODE_700C 6 +#define FREEDV_MODE_700D 7 +#define FREEDV_MODE_2020 8 +#define FREEDV_MODE_700E 13 diff --git a/freedvpkg/README.txt b/freedvpkg/README.txt new file mode 100644 index 0000000..bf20708 --- /dev/null +++ b/freedvpkg/README.txt @@ -0,0 +1,129 @@ + +Note: The directory freedvpkg no longer contains source files. It only contains copies of + the codec2 libraries for use by Quisk. If any other files are present, delete them. + +FreeDV and Directory freedvpkg +============================== + +FreeDV is the combination of the codec2 codec and the fdmdv modem. It provides digital voice +suitable for HF and VHF transmission. Quisk has native (built-in) support for +FreeDV. Just push the FDV mode and talk. This freedvpkg directory contains copies of the +codec2 libraries for use by Quisk. + +You can also use the separate FreeDV program available at freedv.org, and the Quisk DGT-U mode +which attaches to external digital programs. The setup is identical to fldigi and other external +digital programs. + +Quisk will add the FDV button if specified on the Config/radio/Controls tab. +If there is a problem with the freedv module, the button will be grayed out. + +The freedv module requires the codec2 library. This library is included for Windows and for +Ubuntu 18.04 LTS 32-bit and 64-bit. For other systems (such as ARM) you will need to build another +codec2. Just try the FDV mode and see if it works. It should always work on Windows, and may work +on Linux. If the FDV button is grayed out, you need a different codec2 than the one included. Make +a new codec2 library, and copy it to freedvpkg or to the correct system directory. + +2020 mode +========= +You can now use 2020 mode with Quisk. Note however that 2020 mode requires a processor with AVX. +Also note a Raspberry PI (model 4B) does not have enough processing power even if neon is used +and it is overclocked. + +There are two parts to this: +1. build LPCNet +2. rebuild codec2 +The details are found here: https://github.com/drowe67/codec2#freedv-2020-support-building-with-lpcnet + +Both liblpcnetfreedv and libcodec2 are needed. If you are running under windows, the resultant +liblpcnetfreedv.dll file needs to be placed in the quisk folder. Libcodec2 may be placed in the +quisk freedvpkg folder. Or install the DLL's in the correct system folders. + +Search Order +============ + +Quisk will search for a valid codec2 library in this order on Windows: +1. The system codec2 library installed outside of Quisk by another program. +2. freedvpkg/libcodec2.dll. Not included. You can copy your own codec2 here but a system-wide installation is better. +3. freedvpkg/libcodec2_32.dll. The 32-bit codec2 shipped with Quisk. +4. freedvpkg/libcodec2_64.dll. The 64-bit codec2 shipped with Quisk. + +Quisk will search for a valid codec2 library in this order on Linux: +1. The system codec2 library installed outside of Quisk by another program. +2. freedvpkg/libcodec2.so. Not included. You can copy your own codec2 here but a system-wide installation is better. +3. freedvpkg/libcodec2_32.so. The 32-bit codec2 shipped with Quisk. +4. freedvpkg/libcodec2_64.so. The 64-bit codec2 shipped with Quisk. + +How to Build a New codec2 +========================= + +The source for codec2 is available on github. Or google for other sources or perhaps a pre-built library. +If you need to compile codec2 from source, first change to a suitable directory (not the Quisk directory) +and download the source with git: + + git clone https://github.com/drowe67/codec2.git + +Then build codec2 using the directions found at https://github.com/drowe67/codec2/blob/master/INSTALL. +The directions given below are current as of June 2020, but check for changes. +Then copy the codec2 library to the freedvpkg directory under Quisk. + +Build a New codec2 on Linux +=========================== +Create the codec2 shared library. This assumes a 64-bit linux. Change the directory +name from build_linux64 to build_linux32 for 32-bit linux. Note the "../". + + cd codec2 + mkdir build_linux64 + cd build_linux64 + cmake -DCMAKE_BUILD_TYPE=Release ../ + make + cd src + cp libcodec2.so my-quisk-directory/freedvpkg + +Build a New 32-bit codec2 on 64-bit Linux +========================================= +Make sure package libc6-dev-i386 is installed. +Create the codec2 shared library. Note the "../". + + cd codec2 + mkdir build_linux32 + cd build_linux32 + export CFLAGS=-m32 + cmake -DCMAKE_BUILD_TYPE=Release ../ + make + cd src + cp libcodec2.so my-quisk-directory/freedvpkg + +Build a New codec2 on Windows +============================= +For Windows you need to install MinGW-w64, MSYS2, and g++. Use the mingw shell. The Speex libraries +are not needed by codec2, but are required for the Unit Test modules. To build the Unit Test modules, +you need to install Speex and add -DSPEEXDSP_INCLUDE_DIR=../speex/include/speex -DSPEEXDSP_LIBRARY=../speex/bin. + + # Use mingw32.exe 32-bit shell: + cd codec2 + mkdir build_win32 + cd build_win32 + cmake -G "MSYS Makefiles" -DCMAKE_BUILD_TYPE=Release -DUNITTEST=OFF ../ + make codec2 + cd src + cp libcodec2.dll my-quisk-directory/freedvpkg + + # Use mingw64.exe 64-bit shell: + cd codec2 + mkdir build_win64 + cd build_win64 + cmake -G "MSYS Makefiles" -DCMAKE_BUILD_TYPE=Release -DUNITTEST=OFF -DCMAKE_SYSTEM_PROCESSOR=x86_64 ../ + make codec2 + cd src + cp libcodec2.dll my-quisk-directory/freedvpkg + +Testing +======= +You can just start Quisk and see if the FDV button is not grayed out, and FDV works. Or you can +test the import of freedv and look for error messages. + + cd my-quisk-directory + python # Use the correct Python 2 or 3 in 32 or 64 bit + import _quisk + _quisk.freedv_get_version() # This should return 13 or higher for a recent codec2 + diff --git a/freedvpkg/__init__.py b/freedvpkg/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/freedvpkg/__init__.py @@ -0,0 +1 @@ +# diff --git a/freedvpkg/libcodec2_32.dll b/freedvpkg/libcodec2_32.dll new file mode 100644 index 0000000..540842c Binary files /dev/null and b/freedvpkg/libcodec2_32.dll differ diff --git a/freedvpkg/libcodec2_32.so b/freedvpkg/libcodec2_32.so new file mode 100644 index 0000000..8d273d2 Binary files /dev/null and b/freedvpkg/libcodec2_32.so differ diff --git a/freedvpkg/libcodec2_64.dll b/freedvpkg/libcodec2_64.dll new file mode 100644 index 0000000..5971d59 Binary files /dev/null and b/freedvpkg/libcodec2_64.dll differ diff --git a/freedvpkg/libcodec2_64.so b/freedvpkg/libcodec2_64.so new file mode 100644 index 0000000..7d6ebc8 Binary files /dev/null and b/freedvpkg/libcodec2_64.so differ diff --git a/help.html b/help.html new file mode 100644 index 0000000..d09f8e7 --- /dev/null +++ b/help.html @@ -0,0 +1,527 @@ + + + + + + + + + + + + +

+QUISK Help +

+ +

+The current CHANGELOG.txt is here. +
+
+The documentation is here. +
+
+

+ +

This is the Help file for Quisk, a Software Defined Radio (SDR). +Quisk was written and is maintained by Jim Ahlstrom, N2ADR, +www.james.ahlstrom.name. Mail to jahlstr at gmail.com. Quisk has been +greatly improved and extended by Leigh L. Klotz Jr. WA5ZNU, and by Andrew Nilsson VK6JBL. +Thanks to Terry Fox WB4JFI for code improvements, and for adding support +for the Charleston hardware. Thanks to Sid Boyce, G3VBV, for sending me SoftRock hardware to work +with. Thanks to Christof, DJ4CM, for many improvements to the GUI and for the Dx cluster display. +Thanks to Philip G. Lee for adding native support for PulseAudio, and to Eric Thornton for +adding PulseAudio async low-latency support. There are many other contributors who are +mentioned in the source code. +

+

+Quisk supports several radios, such as HiQSDR, SDR-IQ, Hermes-Lite, +SoftRock, etc. The Quisk software will read the samples from UDP or +your sound card data, tune it, filter it, +demodulate it, and send the audio to headphones or speakers. +Quisk can also be used as a panadapter, by sending the radio IF output to Quisk. +

+

+Quisk rhymes with "brisk", and is QSK plus a few letters to make it +easier to pronounce. QSK is a Q signal meaning full break in CW operation, +and Quisk has been designed for low latency. Quisk includes an input +keying signal that can mute the audio and substitute a side tone. +To install and configure Quisk, please see the docs.html file in the +Quisk directory. +

+

+Quisk is written in Python, an easy +to learn but powerful language; see www.python.org. Source is provided +because your own hardware is probably different from mine, and you will +need to change something. Changing Python is easy. +

+ + +

Configuration

+

+Quisk is highly configurable. Many aspects of Quisk such as which buttons +are displayed can be changed. You can even add additional buttons of your own. Quisk +was originally designed to be a general purpose component in anyone's homebrew +software defined radio. Besides the usual configuration settings, +Quisk uses a hardware control file and an optional widgets file. +Quisk comes with a variety of hardware control files for different radios, but +you can also write your own custom hardware file. You can create a custom +quisk_widgets.py file to add custom controls to the Quisk screen. I use custom +files for both. See the n2adr directory for a rather complicated example. +See docs.html for details. +

+

+There are three sources of configuration settings. When Quisk starts, it +first imports default configuration information from the file +quisk_conf_defaults.py. Then it reads your configuration file from either the standard +location, or a custom location given by the command line "-c" option. Your +configuration file should not be a copy of quisk_conf_defaults.py. It should be +a small file with only the items that are different. The use of a configuration file is optional. +

+

+Quisk then reads the settings you have made on the configuration screens. Most settings are +available here, so a configuration file will not be necessary for most users. To reach the +configuration screens, press the Config button; or for the small screen layout, the screen +selection master button followed by the Config button. The configuration screens are on the right side +and are called "radios" because you can save different settings for different radios. There +is a separate help screen describing the radio screens. +

+ + +

QUISK Screens

+

+Quisk can run with either the "Large screen" or "Small screen" layout. The large screen layout +is meant for PC screens, and all buttons are shown in four rows. The small screen layout is +meant for small touch screen devices, for those with sight impairment, or for those who run Quisk at +a small screen size. Most buttons are shown, but the band selection buttons, the mode buttons and the +screen selection buttons are hidden behind three master buttons on the left. Pressing one of the +master buttons pops up a row of choices. +

+

+The screen area can show a frequency graph, a waterfall display, an oscilloscope, the configuration +screens, a graph of the receive filter in use or this Help screen. The screen selection buttons are shown +when you use the large screen layout. Remember to press the master button the see the screen choices if you use the +small screen layout. +

+

+The Graph screen shows a frequency graph of received signals. +Use the scale "Ys" and zero "Yz" controls to adjust the graph scale. +You should see a noise trace that changes +randomly. Then press "Test 1". This generates a test +signal at 10 kHz, and you should see the spike on the graph. To tune +to that signal, click the mouse on the graph near the spike. Hold the +mouse button down and drag the red tuning line back and forth across +the test signal. You should head a pure audio tone in your speakers. +Use the "Vol" (Volume) slider on the left to change the volume. +

+

If you press Graph again, you will activate the peak hold functions +labeled "GraphP1" and "GraphP2". These will cause the graph to +follow the peak signal, and decay back down at a slow rate. You can +configure the time constants. Some buttons, like the Graph button, can be pressed +repeatedly to select different settings. These buttons have a circular arrow on the right. +

+

+The Waterfall shows a time history of the amplitude of received signals. +You will need to adjust "Ys" (Y scale) and "Yz" (Y zero) to get a +colored display. Press "Test 1" to turn the test signal +on and off. Watch it appear and disappear from the waterfall. +The top of the waterfall shows a small graph screen. You can grab the +bar between the screens with your mouse and move it up and down to +adjust the relative sizes. +To adjust the scale and zero of this graph, hold down the Shift key while +using the "Ys" and "Yz" sliders. +

+

+The oscilloscope and RX Filter screens are mostly used for debugging. +The Config screen shows a number of sub-screens that display status, +and provide for control of Quisk parameters. See below. +

+

QUISK Controls

+

+Below the screen area there are one or two slider controls to the left that +control the radio sound volume, and optionally the CW sidetone volume. +Side tone only appears if you configure Quisk to operate as a CW transceiver, and you provide a key signal. +The four sliders to the right control RIT, +the graph and waterfall scale and zero points, and the zoom feature. + +The Rit slider controls receiver incremental tuning, and is active when the RIT button is down. +It adds a small offset to the receive frequency. Leave RIT off for SSB unless +a station is off frequency a bit; then use RIT to tune him in while leaving +your transmit frequency unchanged. When you select CW, the RIT +must be turned on to provide an audio output, so Quisk automatically turns on RIT and sets it +to plus or minus the configured CW tone frequency. The audio side tone (if a hardware key +line is used) is set to the same. Just click CWL or CWU, tune the +frequency by clicking exactly on the signal, and everything will work. + +The Ys and Yz sliders control the scale and zero of +the screen in view. For the Graph screen and the Rx Filter screen +they control the scale and zero of the Y axis. For the Waterfall +screen they control the Waterfall color scale; and if the Shift key is +down, they control the upper graph Y axis. For the Scope screen +the Ys slider controls the Y axis scale, and the Yz slider does +nothing. The sliders have no effect on other screens. +The Zo (zoom) slider expands the frequency scale (X axis) of the Graph +and Waterfall screens so that narrow signals can be examined. +Quisk operates normally when this slider is all the way down. As +it is raised, the frequency is expanded around the tuning +frequency. That is, the tuning frequency is moved to the center, +and the frequency scale is expanded. It is still possible to tune +Quisk as usual while this control is in use. +

+

+The frequency display always displays the transmit frequency. This is the frequency shown +by the red tuning line on the Graph and Waterfall screens. This equals the receive +frequency unless Split is used (see below). +You can change the transmit frequency by clicking +the top or bottom of the digits, or by rolling the mouse wheel over the digit. +The frequency display window will turn red to indicate sample capture overrun (ADC clipping). +The large screen layout has a frequency entry window. Enter a frequency in Hertz without a decimal point, or megahertz with one. +The up and down pointing arrows move the frequency up and down the band without changing the tuning. You can +right or left click them, or hold them down with the mouse to keep moving. +

+ +

+The S-meter displays the signal strength in S units and in dB. Zero dB is +clipping, the same as on the graph screen. The S-meter uses the specified +filter bandwidth to choose the exact number of FFT bins to square and average. +That is, it displays true RMS based on the FFT bins, not on the post-filter audio. Note +that for a noise floor on the graph of -110 dB the S-meter will read -93 dB (depending +on some details). That is +because the bandwidth specified is much greater than the FFT bin width, and more +noise is getting through. Find a flat noise frequency, change the filter bandwidth, +and watch your S-meter measure the noise. The conversion from S-units to dB depends +on your hardware. There are 6 dB per S-unit, and you can adjust to 50 microvolts for S9. +If the correction depends on the band, you can make a band-dependent +correction in your hardware file. +The S-meter window has a button to the +right to select what is shown; the S-meter, the frequency measurement or the RMS audio voltage. +Quisk can measure and display the frequency of a continuous tone RF +signal. To use this feature, press the S-meter button, and +select one of the Frequency items. The numbers are the averaging +times in seconds. Then find a signal of interest and put the +tuning line exactly on it. Quisk will search 500 Hertz up and +down from the tuning line and will display the frequency of the largest +signal. This feature works on AM signals +and continuous signals from oscillators, etc. It does not work +for SSB as there is no continuous signal. To calibrate your +hardware, measure the carrier from WWV +or other frequency standard, and change your clock rate until the +indicated frequency is correct. +Quisk can display the RMS audio voltage. This is used for noise measurements. +

+ +

QUISK Control Buttons

+

+The Quisk buttons are the usual buttons you see on any radio. Some buttons have a cycle symbol +or a cycle button marked ↷ that means the button can cycle through a list of values. Some buttons have +a secondary button that can pop up a menu or a slider to make adjustments. +

+

+Mode: The mode buttons select CW, USB, AM, FM etc. This is a master button in the small screen layout. +The special mode IMD generates a two-tone test signal for transmitter testing. The "DGT" modes are +for external digital mode programs such as fldigi. See below. +

+Band: The band buttons select the band 40 meters, 20 meters etc. The bands shown can be configured. +This is a master button in the small screen layout. +

+Screen: The screen button selects the screen that is shown. See above. +This is a master button in the small screen layout. +

+Filters: The filter buttons select the bandwidth of the receive filters, 2700 Hertz, 6000 Hertz etc. +The last filter button has an adjustable bandwidth. +

+Band Up/Down: The up and down arrow buttons move up and down the band without changing the tuning frequency. +

+Mute: Mute (zero) the receive audio volume. +

+AGC: Automatic gain control is active when this button is either up or down, but the settings are different. +Adjust the AGC for both the up and down positions to control how much gain variation is allowed. When the +slider is at maximum (all the way up), all signals, even band noise, +will have the same maximum amplitude. For lower settings, loud +signals will have the maximum amplitude, but weaker signals will have +less amplitude. A medium setting allows you to hear the relative +amplitude of signals and any QSB while still protecting your +ears. I set the AGC On setting to a high value, and the AGC Off +setting to a lower value that allows band noise to be faintly heard. +

+Squelch: This turns off the audio when there are no signals in the pass band. For FM, adjust the level +on an empty channel. For SSB the adjustment for band noise is automatic, so start with a setting of 0.200. +

+NB: The noise blanker has several levels. +

+Notch: An automatic notch feature that can null out one or two continuous signals, such +as AM carriers that interfere with SSB reception. +

+RfGain: The Rf gain control if your hardware supports this. +

+Antenna: Antenna selection if your hardware supports this. +

+Spot: This transmits a constant CW signal for tuning. The level is adjustable. You might also need to press PTT +or assert your key line depending on your hardware. +

+Split: This splits the receive and transmit frequencies so they can be different. Two tuning lines +will appear; red for the transmit frequency and green for the receive frequency. This is useful for +working a DX station split. When you tune with the mouse, the closest tuning line is moved. +You can lock the transmit frequency. You can reverse the receive and transmit frequencies. +You can decode audio from both frequencies and play them on the left and right speakers. +

+FDX: Full duplex allows you to transmit and receive at the same time if your hardware allows this. +

+PTT: Push to talk; start transmitting. +

+VOX: Voice operated relay; turn on PTT when you speak into the microphone. +

+Test 1: Generate a test signal 10 kHz above the screen center frequency. +

+Memory buttons: +Quisk can remember and return to stations. When you have tuned in +a signal of interest, press the "Save" button Ⓜ↑ to save the frequency, +band and mode. Repeat for more +signals. Now press "Next" Ⓜ ➲, +to switch to the next saved signal, and press "Next" repeatedly to +cycle through the list. To delete a saved signal, first tune to +it with "Next" and then press "Delete" Ⓜ ☓ . If you save a large +number of signals, right click the "Next" button Ⓜ ➲, and you will get a +popup menu so you can jump directly to a station. The saved +stations will appear in the station window below the frequency X axis. +The saved stations can be on different bands. +

+Favorites button: +Quisk can save a list of favorite stations. Press +Config/Favorites to access the screen, and right-click the left label +to insert and delete stations, and to tune to them. The two +favorites buttons provide direct access to this screen. The +★ ↑ button enters the current station into the screen; just +provide a name. The ★ ↓ button jumps to the screen; +right click the left label and choose "Tune to". Favorite +stations will appear in the station window with a Star symbol ★ . +

+ +

Recording Sound and Samples

+ +

+There are Record and Playback buttons to save and recall radio sound from a temporary buffer. Push +Record to start recording radio sound. The maximum time to record +can be configured, and after this limit +older sound is discarded. That is, the most recent sound is +retained up to some maximum time. Push Playback to play the sound. If you are +transmitting, the recorded sound is transmitted in place of mic +input. This sound is not speech processed, so it can be used to +give another station an accurate indication of how they sound. +To write this sound to a file, right-click the Playback button. +

+

+Quisk can also record the speaker audio, mic audio and the digital I/Q samples to WAV +files. Set the file names in the Config/Config screen. Enable the recording by checking +the box. Then Press +the "FileRecord" button to begin recording, and release it to stop. +If you press it again, it starts a new recording with a sequence number 001, 002, 003 etc. +Choose a directory (folder) to save these files because you will need to delete the ones you no longer need. +The speaker and mic audio are stored as 16-bit monophonic +samples at the audio play rate. +Both the audio play and mic rates should be 48 ksps. +The digital I/Q samples are stored as two +IEEE floating point samples at the sample rate. +It is possible to record all three sources at once, but this is not usually useful. +Normally you would record the mic to create a CQ message such as "CQ contest this is N2ADR". +You would record speaker audio to create a record of operations to review at a later time. +You would record IQ samples so you could record a slice of the band to tune in different +stations later. +Note that a WAV file has a maximum size of 4 gigabytes, but Quisk can record and play +files with an unlimited size. +

+

+Quisk can play the files it created, as well as other WAV files in the proper format. +Choose the file name of the source on the Config/Config screen, and check the box. +Then press "FilePlay" to begin playing and release it to stop. +Playing a file replaces the usual samples with the file samples, so it is necessary to +have working sound sources at the same sample rates as the recording. +Playing an audio file just plays the file on the speakers. When the file is finished, +normal speaker audio resumes. Playing a CQ message is similar, but will press the PTT button +as it plays. You can specify a repeat time to keep repeating the message. When a station +answers, press FilePlay or PTT to stop the message so you can answer. +

+

+To play an IQ sample file, first make sure the sample rate and VFO frequency are the same as +the recording. This will ensure the frequency readout is correct. It is a good idea to name files +with this information, such as "IQ192k7100.wav" for a file recorded at 192 ksps and a VFO (center) +frequency of 7100 kHz. Then press "FilePlay". The band samples will be replaced with the file samples, +and you will be able to tune around in the band and receive different stations. Do not press band up/down +as that will change the band center (VFO). +

+ +

Tuning in Stations

+ +

+First select CW, USB etc. with the mode buttons. This is a master button on the small screen layout. +On the Graph or Waterfall screens, you tune in a CW signal by left-clicking +above the X axis directly on the signal. You tune in an SSB signal by clicking on the upper edge +(lower sideband) or the lower edge (upper sideband). That is, you always click +where the carrier goes. You can also click, hold down the +mouse button and drag the tuning line. The speed of tuning is lowest +close to the X axis, and increases as you move up. Try it. +If you hold the Shift key down when you click, the Rx filter will be centered at the frequency. +In this case, you would click in the center of an SSB signal. +

+ +

+If you click below the X axis, tuning will not jump to that frequency, but +you can still hold the button and drag. That is useful for small +adjustments. The mouse wheel will move the frequency up and down and round the +frequency to multiples of 50 Hertz. +

+ +

+If you right-click a signal, Quisk tunes to the signal as before, and also +changes the VFO to move the +signal to the center of the screen. +

+ +

Station Window and Dx Cluster

+

+There is a station window below the frequency axis (X axis) to display +stations of interest. This feature was added by Christof, +DJ4CM. It consists of zero or more lines containing an M-Circle +symbol Ⓜ for memory stations, a Star symbol ★ for +favorite stations and a Dx symbol for Dx Cluster stations. +The default number of lines is one, but you can add more +lines if the display becomes too crowded. If you move your mouse +near the marked frequency, a popup window will appear with station +details. Left-click the symbol to tune to it. +There are Dx Cluster telnet servers available that provide real time +information on Dx station activity. You must configure the host name, the port +and your call sign to use this feature. +This feature will run continuously unless the host is the +null string. The Dx stations will appear in the station window as +they become available from the server. +

+ + +

Configuration Screens

+ +

+The Configuration screen shows a group of sub-screens that give status information, +and that adjust various Quisk control parameters. +The Status screen shows Quisk status information. + +The Favorites screen allows you to enter frequencies and modes for +stations, nets, etc. To add a line, right-click the left label +and select Insert or Append. To tune to a station, select +Tune. +

+ +

+The Config screen controls features that apply to all radios. There is a band plan button +to control the colors shown for the CW and phone segmants of each band. +The Config screen can pop up an +amplitude and phase correction window for SoftRock and similar receivers. +If you use the sound card for input, you may need to correct for small errors +in the I and Q amplitude and phase. First change to the correct band, because +corrections are saved for each band. Press the button on the config +screen to bring up a correction screen. Then feed a test signal to your hardware +(or use a strong available signal) and look at the signal image. Adjust the +slider controls to reduce the image. The upper slider is the fine adjustment, +and the lower is the coarse. You will need to adjust both amplitude and phase +as they will interact. The amount of available adjustment range +can be configured. When +you have a final correction, it is a good idea to write it down. +The correction point is saved based on the VFO frequency, and you will +probably need two or three correction points per band. +

+

+The Tx Audio screen controls the transmit audio, and +enables you to record and play it back for test purposes. +To have good transmit audio, you must start start with a clean +audio source. You can plug in a USB mic or headset as a +source. Or you can connect an analog mic to the mic input of your +sound card. Or you can connect your mic to a preamp, and then to +the line input of your sound card. The mic should be directly in +front of your mouth when you speak. Try to avoid headsets that +cause you to speak across the mic. The idea is to make your voice +as loud as possible relative to the background noise in your shack. + +

+

+ +You need to adjust the mic sound level as high as possible, but without +too much clipping. Speak normally and try to get a level a few dB below +zero (clipping). The peak level should be at least -20 dB, and +preferably above -10 dB. Infrequent clipping (above 0 dB) is OK, +because Quisk will clip the audio anyway when it processes it. It +may be difficult to figure out how to adjust the mic level. For +Linux, figure out the correct control number and use mixer_settings +or use one of the Linux mixer apps. +For Windows, use the level +control on the audio control screen in Control Panel, but be careful +that another application does not change it after you set it for +Quisk. Quisk will attempt to adjust the audio level with its AGC, +but it helps to start with a good level. + +

+

+ +Refer to the "Tx Audio" tab. Clip is the amount of audio clipping +from zero to 20 dB. There is actually some clipping at zero dB +due to the mic AGC. Preemphasis boosts the high frequencies in +your voice. Zero is no boost, and 1.0 is 6 dB per octave. +Use the record and playback buttons to test for the best control +settings. Notice that your voice will become louder with more +clipping. Note that SSB, AM, FM and FDV each have their own settings, +so change to the correct mode before you start. Audio processing +is most useful for SSB, so if you are a DX enthusiast, use aggressive +settings. I only use AM for rag chewing, so I use zero +preemphasis and 4 dB clipping. For FM, I use zero clipping and +preemphasis. + + +

Digital Modes DGT-*

+ +

+These modes are used for digital signals, and require an external +program such as Fldigi or WSJT-X to decode the signals. +First, connect your digital program +to quisk using Hamlib or XML-RPC so that frequency changes are recognized. +See "Hamlib Control" below. Next you need a way to transfer digital samples between the programs. +For Windows, install a Virtual Audio Cable (VAC) and connect Quisk to one end and the +digital program to the other. For Linux, Quisk can create a VAC itself by using PulseAudio. +Digital data is only sent when one of the "DGT-" modes is in use. +See the documentation for more information.

+ +

Hamlib Control

+ +

+Most digital and logging programs use Hamlib to control +a rig so that frequency changes in one program are recognized in the other. +Quisk has three options for external control: Hamlib, XML-RPC and a serial port emulating the "Flex" protocol. +See the documentation for more information.

+

+ +

Multiple Receivers

+ +

+Quisk can tune in two frequencies in the same band by using the "Split" button. +This is meant for working DX split, but it can be used anywhere in the band. +But if you want to receive in two different bands at once, you need SDR hardware that +can support multiple sub-receivers. The Hermes-Lite and other Hermes hardware can do this. +Press the "Add Rx" button to add additional receivers. You will get multiple receiver +screens with their own control buttons and menus. The main buttons in Quisk still control +the main receiver and transmitter. The menu button on the "Add Rx" button controls where +the sub-receiver sound goes. When a sub-receiver "Play" button is pressed, you can play the sound +alone, or together with the main receiver sound. There is an additional device on the sound screen +that can direct the sound from sub-receiver 1 to an external digital program. +There is a limit to how many sub-receivers your hardware can support. Quisk may not know this +limit, so please do not request more sub-receivers than actually exist. +

+ + +

+ + diff --git a/help_vna.html b/help_vna.html new file mode 100644 index 0000000..3d25ba0 --- /dev/null +++ b/help_vna.html @@ -0,0 +1,144 @@ + + + + + + +QUISK Help File + +

+Quisk VNA Help (December 2016) +

+ + +

This is the Help file for Quisk VNA, a program that turns the +Quisk2010 and HiQSDR transceiver and the Hermes-Lite transceiver into a Vector Network Analyzer +(VNA). This Help appears when you press the Help button. Quisk is written by Jim +Ahlstrom, N2ADR, +www.james.ahlstrom.name. Mail to jahlstr at gmail.com. To run the +Quisk VNA program, use "python quisk_vna.py" or set up a +shortcut. This program only works with hardware that is based +on UDP. It does not work with SoftRock hardware. +

+

For HiQSDR you need to update your firmware to Version 1.3 or later. +For Hermes-Lite update to 32 or later. The +new firmware locks the phase of the RF output to the phase of the RF +detector. There is a time delay in the path, but this is removed +by the calibration procedure. The calibration graphs will show a linear phase change with frequency due to this delay. +

+

There are two ways to use your hardware. You could connect the +RF output through attenuators, through a device under test and then +back to the RF input. This is called transmission mode. It is +used to plot the response of filters and to measure the electrical +length of cables. Or you could connect the RF input and output through +attenuators to +a resistive return loss bridge. This is called reflection mode. It is +used to measure SWR and impedance. You must choose a mode and +calibrate for that mode and hardware before you take any data. +The calibration is saved and will be restored the next time the +program starts. This is convenient when making repeated measurements, +but you should calibrate frequently for best accuracy. Hermes-Lite requires a power +on/off and a calibration before taking data, but the calibration is good until it is powered down. +

+

+The hardware generates RF output only when one of the "Calibrate" +buttons or the "Run" button is turned on. +The calibration routines save data every 15 kHz from zero +to sixty megahertz (thirty for Hermes-Lite). You can change the measurement frequency span +at any time without recalibration. Although you can set a +frequency span up to sixty (thirty) megahertz, my original hardware has a low +pass filter cutoff of 35 megahertz, so the upper frequency range is +smaller and will appear noisy. +Since the transmit and receive frequencies are equal, the data is at +DC, and is averaged and effectively low pass filtered. This +provides immunity from interference when measuring an antenna. +

Input Protection

+Do not connect the RF output to the RF input without inserting +attenuators. The output will overload the input and result in +clipping in the ADC and possible damage. If your device under +test is an amplifier be especially careful to add additional +attenuation to avoid damage. Add enough attenuation to avoid +clipping, but not so much that you lose dynamic range. The +calibration screens show the ADC level. These attenuators also +help to stabilize the input and output impedance and increase +accuracy. I use the HAT series of attenuators from Mini-Circuits. + + +

Transmission Mode +

+ +You must perform a calibration before you can take data. First +set the mode to "Transmission" and leave it there. Connect attenuators +and a cable between the RF input and output. Press the "Calibrate.." button. +With the cable connected press "Short". The "Short" calibration is +required for transmission mode. For the optional "Open" calibration, disconnect +the cable and press "Open". Then press "Calibrate" to save the calibration. +Connect the cable and press "Run" and +you should see a flat line at zero dB level and zero phase. Now +insert your test device in series. The test device could be a +filter, an amplifier (be careful) or an additional length of +cable. Press "Run" to take data. + +

Reflection Mode +

+

+This mode is used with a resistive return loss bridge. +Connect the RF +output to the generator terminal, and the RF input to the detector +terminal. Use attenuators on both. You must +perform a calibration before you can take data. +First set the +mode to "Reflection" and leave it there. Press the "Calibrate.." button. +Connect an open circuit (or nothing) to +the impedance +terminal of the bridge, and then press "Open". Connect a +short circuit to the bridge, and then press "Short". Connect +a 50 ohm termination to the bridge and then press "Load". +Then press "Calibrate" to save the calibration. +If you do not have a set of Open/Short/Load standards, you can just +use "Open" alone, but it is highly +recommended to use Open, Short and Load. +

+

+Now connect a 50 ohm termination to the bridge, and +press "Run". The graph will show the magnitude and phase of the +reflection coefficient, and the return loss is the drop in magnitude +below zero dB. Ideally your bridge will have a directivity of 30 +dB or more. The phase may be noisy if the magnitude is very +small. Now connect an +unknown impedance to the bridge; for example, an antenna. The +graph will plot the return loss, reflection coefficient and SWR. +The status line will display the frequency, the reflection coefficient and SWR, the impedance, the equivalent +capacitance or inductance for that impedance, and the values for the parallel equivalent circuit. +

+

+You can attach any impedance, such as an unknown capacitor or inductor, +and read the value directly. The value may seem to vary with +frequency due to stray inductance, variation of permeability with +frequency, and bridge imperfections; so choose a reference frequency +wisely. Remember that the bridge measures the impedance relative +to fifty ohms, so accuracy suffers if the impedance is outside the +range of 5 to 500 ohms or so. +

+

Fun

+

+In transmission mode, add an extra length of cable and see the phase +change. When the phase change is ninety degrees, that is a +quarter wave. The effect of velocity factor is included, and can +be measured. Use a bare wire (with attenuators) as the test +fixture, and then add a ferrite bead to the wire to measure its +properties. Insert a filter to see its response. +

+

+In reflection mode, measure your antenna from zero to sixty (thirty) +megahertz. If it is a dipole, you will see the drop in SWR at its fundamental and +third harmonic. Add a cable to the bridge to see its impedance at +a quarter and half wave length. Measure the length of your +transmission line by replacing your antenna with a 100 ohm +resister. The bridge will read 100 ohms at multiples of a half +wavelength. If you short out your antenna at the far end of your +transmission line, the bridge will read zero at half wavelengths, and +infinity at quarter wavelengths. +

+ + diff --git a/hermes/README.txt b/hermes/README.txt new file mode 100644 index 0000000..19ed139 --- /dev/null +++ b/hermes/README.txt @@ -0,0 +1,65 @@ +This directory has files for the Hermes-Lite project versions 1 and 2. +Please perform all configuration with the Config screens in Quisk. +To start, go to Config/Radios and add a new radio with the general type "Hermes". + +Quisk fully supports the Hermes-Lite project. But you may need a custom Widget or +Hardware file for special purposes. If you create these custom files, enter their +location (path) on the Config/radio/Hardware screen. Do not modify the files supplied +by Quisk, as these will be replaced with each new release. + + + +# This is an example of a custom Widgets file. It changes the power calculations +# to that required by an external power bridge. Any methods (functions) defined here are used +# instead of the usual versions. See the original quisk_widgets.py to see what is available +# for replacement. + +from hermes.quisk_widgets import BottomWidgets as BaseWidgets + +class BottomWidgets(BaseWidgets): # Add extra widgets to the bottom of the screen + # This replaces the default version. You must alter the code to calculate watts for your + # external power meter that is connected to the Hermes-Lite power ADC. + def Code2FwdRevWatts(self): # Convert the HermesLite fwd/rev power code to watts forward and reverse + # volts = m * code + b # The N2ADR power circuit is linear in voltage + # power = (m**2 * code**2 + 2 * b * m * code + b**2) / 50 + fwd = self.hardware.hermes_fwd_power # forward and reverse binary code + rev = self.hardware.hermes_rev_power + Vfwd = 3.26 * fwd / 4096.0 # forward and reverse volts + Vrev = 3.26 * rev / 4096.0 + Pfwd = 2.493 * Vfwd**2 + 0.1165 * Vfwd # conversion from volts to power in watts + Prev = 2.493 * Vrev**2 + 0.1165 * Vrev + return Pfwd, Prev # return forward and reverse power in watts + + + +# This is an example of a custom Hardware file: + +from hermes.quisk_hardware import Hardware as BaseHardware + +class Hardware(BaseHardware): + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.usingSpot = False # Use bit C2[7] as the Spot indicator + def ChangeBand(self, band): + # band is a string: "60", "40", "WWV", etc. + # The call to BaseHardware will set C2 according to the Hermes_BandDict{} + ret = BaseHardware.ChangeBand(self, band) + if self.usingSpot: + byte = self.GetControlByte(0, 2) # C0 index == 0, C2: user output + byte |= 0b10000000 + self.SetControlByte(0, 2, byte) + return ret + def OnSpot(self, level): + # level is -1 for Spot button Off; else the Spot level 0 to 1000. + ret = BaseHardware.OnSpot(self, level) + if level >= 0 and not self.usingSpot: # Spot was turned on + byte = self.GetControlByte(0, 2) + byte |= 0b10000000 + self.SetControlByte(0, 2, byte) + self.usingSpot = True + elif level < 0 and self.usingSpot: # Spot was turned off + byte = self.GetControlByte(0, 2) + byte &= 0b01111111 + self.SetControlByte(0, 2, byte) + self.usingSpot = False + return ret diff --git a/hermes/__init__.py b/hermes/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/hermes/__init__.py @@ -0,0 +1 @@ +# diff --git a/hermes/quisk_conf.py b/hermes/quisk_conf.py new file mode 100644 index 0000000..42baa14 --- /dev/null +++ b/hermes/quisk_conf.py @@ -0,0 +1 @@ +# Configuration files are no longer used. Please use the radio Config screens instead. diff --git a/hermes/quisk_conf2.py b/hermes/quisk_conf2.py new file mode 100644 index 0000000..42baa14 --- /dev/null +++ b/hermes/quisk_conf2.py @@ -0,0 +1 @@ +# Configuration files are no longer used. Please use the radio Config screens instead. diff --git a/hermes/quisk_hardware.py b/hermes/quisk_hardware.py new file mode 100644 index 0000000..5e054d6 --- /dev/null +++ b/hermes/quisk_hardware.py @@ -0,0 +1,1221 @@ +# This is a sample hardware file for UDP control using the Hermes-Metis protocol. Use this for +# the HermesLite project. It can also be used for the HPSDR, but since I don't have one, I +# can't test it. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import socket, traceback, time, math, os +import wx +import _quisk as QS +import quisk_utils + +from quisk_hardware_model import Hardware as BaseHardware + +DEBUG = 0 +DEBUG_I2C = 0 + +class IOBoard: + "This class controls the N2ADR IO Board for the HermesLite 2" + REG_TX_FREQ_BYTE4 = 0 + REG_TX_FREQ_BYTE3 = 1 + REG_TX_FREQ_BYTE2 = 2 + REG_TX_FREQ_BYTE1 = 3 + REG_TX_FREQ_BYTE0 = 4 + REG_CONTROL = 5 + REG_RF_INPUTS = 11 + REG_FAN_SPEED = 12 + REG_FCODE_RX1 = 13 + REG_FCODE_RX2 = 14 + REG_FCODE_RX12 = 24 + REG_ADC0_MSB = 25 + REG_ADC0_LSB = 26 + REG_ADC1_MSB = 27 + REG_ADC1_LSB = 28 + REG_ADC2_MSB = 29 + REG_ADC2_LSB = 30 + def __init__(self, hardware): + self.DEBUG = 0 + self.hardware = hardware + self.have_IO_Board = None + self.have_board_counter = 3 + self.tx_timer = 0 + self.current_tx_freq = 0 + self.current_vfo = 0 + self.old_receive = None + self.slow = 0 + def HeartBeat(self): # Called at 10 Hz for housekeeping tasks + if not QS.get_params('rx_udp_started'): + return + if self.have_IO_Board is None: + resp = self.hardware.ReadI2C(0x7d, 0x41, 0) # Check for the N2ADR HL2 IO Board + if resp and resp[1] == 0xF1: + self.have_IO_Board = True + if self.DEBUG or DEBUG_I2C: + print ('Have IO_Board') + self.hardware.WriteI2C(0x7d, 0x1D, self.REG_CONTROL, 1) + if self.DEBUG: print ("IO Board RESET") + self.hardware.ImmediateChange('hermes_iob_rxin') + else: + self.have_board_counter -= 1 + if self.have_board_counter == 0: + self.have_IO_Board = False + if self.DEBUG or DEBUG_I2C: + print ('No IO board') + if not self.have_IO_Board: + return + if self.hardware.vfo_frequency != self.current_vfo: # defeat phase error in ChangeFrequency() + self.current_vfo = self.hardware.vfo_frequency + self.NewRxFreq(0, self.current_vfo) + if self.hardware.tx_frequency != self.current_tx_freq and time.time() - self.tx_timer > 0.50: + self.current_tx_freq = self.hardware.tx_frequency + self.tx_timer = time.time() + tx = self.current_tx_freq # Send Tx frequency to IO Board + if self.DEBUG: print ("IO Board TxFreq", tx) + self.hardware.WriteI2C(0x7d, 0x1D, self.REG_TX_FREQ_BYTE4, (tx >> 32) & 0xFF) # MSB + self.hardware.WriteI2C(0x7d, 0x1D, self.REG_TX_FREQ_BYTE3, (tx >> 24) & 0xFF) + self.hardware.WriteI2C(0x7d, 0x1D, self.REG_TX_FREQ_BYTE2, (tx >> 16) & 0xFF) + self.hardware.WriteI2C(0x7d, 0x1D, self.REG_TX_FREQ_BYTE1, (tx >> 8) & 0xFF) + self.hardware.WriteI2C(0x7d, 0x1D, self.REG_TX_FREQ_BYTE0, tx & 0xFF) # LSB + if self.DEBUG > 1: + self.slow += 1 + if self.slow >= 10: + self.slow = 0 + ret = self.Receive(self.REG_ADC0_MSB) + #print ('IO Board ADC: 0x%X 0x%X 0x%X 0x%X' % tuple(ret)) + v0 = (ret[0] << 8 | ret[1]) / 4096.0 * 3.0 + v1 = (ret[2] << 8 | ret[3]) / 4096.0 * 3.0 + #print ('IO Board ADC 0 %4.3f ADC1 %4.3f' % (v0, v1)) + ret = self.Receive(self.REG_TX_FREQ_BYTE3) + if ret and ret != self.old_receive: + self.old_receive = ret + f = ret[3] | ret[2] << 8 | ret[1] << 16 | ret[0] << 24 + print ('IO Board Freq: 0x%X 0x%X 0x%X 0x%X %d' % (tuple(ret) + (f,))) + def Receive(self, register): # Get the response from the IO board + if not self.have_IO_Board: + return None + ret = self.hardware.ReadI2C(0x7d, 0x1D, register) + if not ret: + return ret + # Return four registers in numeric order + ret.reverse() + return ret[0:4] + def FanLevel(self, level): + if not self.have_IO_Board: + return + # Send the fan level as I2C register 2 + # level is an integer from 0 to 255 + if self.DEBUG: print ("IO Board: Fan level", level) + self.hardware.WriteI2C(0x7d, 0x1D, self.REG_FAN_SPEED, level) + def AuxRxInput(self, mode): + # 0: The HL2 operates as usual. The receive input is not used. The Pure Signal input is available. + # 1: The receive input is used instead of the usual HL2 Rx input. Pure Signal is not available. + # 2: The receive input is used for Rx, and the Pure Signal input is used during Tx. + if not self.have_IO_Board: + return + if self.DEBUG: print ("IO Board: Rx mode", mode) + self.hardware.WriteI2C(0x7d, 0x1D, self.REG_RF_INPUTS, mode) + def hertz2code(self, freq): # frequency codes for the IO board + if freq == 0: + return 0 + code = int(0.5 + 15.47 * math.log(freq / 18748.1)) + if code < 1: + return 1 + elif code > 255: + return 255 + return code + def code2hertz(self, code): + if code == 0: + return 0 + freq = int(0.5 + 18748.1 * math.exp(code / 15.47)) + return freq + def NewRxFreq(self, index, freq): + if not self.have_IO_Board: + return + if 0 <= index < 12: + fcode = self.hertz2code(freq) + self.hardware.WriteI2C(0x7d, 0x1D, self.REG_FCODE_RX1 + index, fcode) + if self.DEBUG: + print ("IO Board RxFreq index %d freq %d code %d" % (index, freq, fcode)) + def Antenna(self, Tx, Rx): + if not self.have_IO_Board: + return + ant = Tx << 4 | Rx + if self.DEBUG: print ("IO Board: antenna 0x%X" % ant) + self.hardware.WriteI2C(0x7d, 0x1D, 31, ant) + +class Hardware(BaseHardware): + var_rates = ['48', '96', '192', '384'] + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.var_index = 0 + self.hermes_mac = bytearray(6) + self.hermes_ip = "" + self.hermes_code_version = -1 + self.hermes_board_id = -1 + self.hermes_temperature = 0.0 + self.hermes_fwd_power = 0.0 + self.hermes_rev_power = 0.0 + self.hermes_pa_current = 0.0 + self.eeprom_valid = 0 + self.mode = None + self.band = None + self.vfo_frequency = 0 + self.tx_frequency = 0 + self.vna_count = 0 + self.vna_started = False + self.repeater_freq = None # original repeater output frequency + self.antenna_labels = ('Ant 0', 'Ant 1') # labels for the antenna button + self.antenna_index = 0 # index of antenna in use + self.delay_config = True # Delay sending message to HL2 until after sound starts + self.TFRC_counter = 0 # Call for power etc. at intervals + self.key_was_down = 0 + self.io_board = IOBoard(self) + try: + self.repeater_delay = conf.repeater_delay # delay for changing repeater frequency in seconds + except: + self.repeater_delay = 0.25 + self.repeater_time0 = 0 # time of repeater change in frequency + # Create the proper broadcast addresses for socket_discover + if conf.udp_rx_ip: # Known IP address of hardware + self.broadcast_addrs = [conf.udp_rx_ip] + else: + self.broadcast_addrs = [] + for interf in QS.ip_interfaces(): + broadc = interf[3] # broadcast address + if broadc and broadc[0:4] != '127.': + self.broadcast_addrs.append(broadc) + self.broadcast_addrs.append('255.255.255.255') + if DEBUG: print ('broadcast_addrs', self.broadcast_addrs) + # This is the control data to send to the Hermes using the Metis protocol + # Duplex must be on or else the first Rx frequency is locked to the Tx frequency + # + # From the protocol document, the data are: C1==[31:24], C2=[23:16], C3=[15:8], C4=[7:0] where [] are inclusive. + # self.pc2hermes[] is used for the first 17 addresses 0x00 through 0x10. These are sent round-robin. + self.pc2hermes = bytearray(17 * 4) # Control bytes not including C0. Python initializes this to zero. + # Addresses C0 = 0x12 through 0x3E use self.pc2hermeswritequeue, which is capable of requesting an ACK. + # C0 is a six-bit address. If the address is OR'd with 0x40, an ACK must be received or the item is re-transmitted. + # The item sent to the HL2 is C0<<1 | MoxBit. + # Sound must be started before you can use pc2hermeswritequeue. + # General I2C bus Access: + # Address C0 = 0x3c writes to the I2C bus 1. Use C0 = 0x7c to request an ACK. + # Address C0 = 0x3d writes to the I2C bus 2. Use C0 = 0x7d to request an ACK. + # Read requests must use the ACK. + # self.pc2hermeslitewritequeue[0:5] = C0, C1, C2, C3, C4 + # C1 = 0x06 to write, 0x07 to read + # C2 = 7-bit I2C address | stop; Where "stop" is 0x80 to stop at end; else zero for continue + # C3 = 8-bit I2C control (often a register address) + # C4 = 8-bit I2C data for write, else 0 + self.pc2hermeslitewritequeue = bytearray(5) + # Initialize some data + self.pc2hermes[3] = 0x04 # C0 index == 0, C4[5:3]: number of receivers 0b000 -> one receiver; C4[2] duplex on + self.pc2hermes[4 * 9] = 63 # C0 index == 0b1001, C1[7:0] Tx level + for c0 in range(1, 9): # Set all frequencies to 7012352, 0x006B0000 + self.SetControlByte(c0, 2, 0x6B) + self.ChangeTxLNA(conf.hermes_TxLNA_dB) + self.MakePowerCalibration() + def pre_open(self): + # This socket is used for the Metis Discover protocol + self.discover_request = b"\xEF\xFE\x02" + b"\x00" * 60 + self.socket_discover = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket_discover.setblocking(0) + self.socket_discover.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + found = False + st = "No capture device found." + port = self.conf.rx_udp_port + for i in range(5): + if found: + break + if DEBUG: print ('Send discover') + try: + for broadcast_addr in self.broadcast_addrs: + self.socket_discover.sendto(self.discover_request, (broadcast_addr, port)) + if DEBUG: print ('discover_request', (broadcast_addr, port)) + time.sleep(0.01) + except: + if DEBUG > 1: traceback.print_exc() + for j in range(5): + try: + data, addr = self.socket_discover.recvfrom(1500) + except: + if DEBUG > 1: traceback.print_exc() + time.sleep(0.02) + continue + else: + if DEBUG: print('recvfrom', addr, 'length', len(data), "type", type(data)) + data = bytearray(data) + if len(data) > 32 and data[0] == 0xEF and data[1] == 0xFE: + if DEBUG: print('data', data) + ver = self.conf.hermes_code_version + bid = self.conf.hermes_board_id + if ver >= 0 and data[9] != ver: + pass + elif bid >= 0 and data[10] != bid: + pass + else: + if data[10] == 6: # Hermes Lite + num_rx = int(data[0x13]) + if num_rx < 1: + num_rx = 1 + elif num_rx > 10: + num_rx = 10 + else: + num_rx = 1 + dta = data[3:10] + dta.append(data[0x15]) + dta.append(num_rx) + dta.append(data[10]) + st = 'Capture from Hermes: Mac %2x:%2x:%2x:%2x:%2x:%2x, Code version %d.%d, Rx %d, ID %d' % tuple(dta) + self.hermes_mac = data[3:9] + self.hermes_ip = addr[0] + self.hermes_code_version = data[9] + self.hermes_board_id = data[10] + QS.set_hermes_id(data[9], data[10]) + if data[0x16] >> 6 == 0: + QS.set_params(bandscopeScale = 2048) + if DEBUG: print (st) + adr = self.conf.rx_udp_ip + found = True + if adr and adr != addr[0]: # Change the IP address + if DEBUG: print("Change IP address from %s to %s" % (addr[0], adr)) + ip = adr.split('.') + ip = list(map(int, ip)) + cmd = bytearray(73) + cmd[0] = 0xEF + cmd[1] = 0xFE + cmd[2] = 0x03 + cmd[3] = data[3] + cmd[4] = data[4] + cmd[5] = data[5] + cmd[6] = data[6] + cmd[7] = data[7] + cmd[8] = data[8] + cmd[9] = ip[0] + cmd[10] = ip[1] + cmd[11] = ip[2] + cmd[12] = ip[3] + for broadcast_addr in self.broadcast_addrs: + self.socket_discover.sendto(cmd, (broadcast_addr, port)) + time.sleep(0.01) + # Note: There is no response, contrary to the documentation + self.hermes_ip = adr + if False: + try: + data, addr = self.socket_discover.recvfrom(1500) + except: + if DEBUG: traceback.print_exc() + else: + print(repr(data), addr) + ##self.hermes_ip = adr + time.sleep(1.0) + st += ', IP %s' % self.hermes_ip + break + if not found and self.conf.udp_rx_ip: + self.hermes_ip = self.conf.udp_rx_ip + code = 62 + bid = 6 + self.hermes_code_version = code + self.hermes_board_id = bid + QS.set_hermes_id(code, bid) + st = 'Capture from Hermes device at specified IP %s' % self.hermes_ip + found = True + if found: + # Open a socket for communication with the hardware + msg = QS.open_rx_udp(self.hermes_ip, port) + if msg[0:8] != "Capture ": + st = msg # Error + self.socket_discover.close() + self.config_text = st + self.ChangeLNA(2) # Initialize the LNA using the correct LNA code from the FPGA code version + def open(self): + self.delay_config = True # Delay sending message to HL2 until after sound starts + # This list only changes control bits; no use of WriteQueue() + for name in ('keyupDelay', + 'hermes_lowpwr_tr_enable', 'hermes_PWM', 'hermes_disable_sync', 'hermes_power_amp', 'Hermes_BandDictEnTx'): + self.ImmediateChange(name) + return self.config_text + def GetValue(self, name): # return values stored in the hardware + if name == 'Hware_Hl2_EepromIP': + addr1 = self.ReadEEPROM(0x08) + addr2 = self.ReadEEPROM(0x09) + addr3 = self.ReadEEPROM(0x0A) + addr4 = self.ReadEEPROM(0x0B) + if addr1 < 0 or addr2 < 0 or addr3 < 0 or addr4 < 0: + return "Read failed" + else: + return "%d.%d.%d.%d" % (addr1, addr2, addr3, addr4) + elif name == 'Hware_Hl2_EepromIPUse': + use = self.ReadEEPROM(0x06) + if use < 0: + return "Read failed" + if not use & 0b10000000: + return 'Ignore' + elif use & 0b100000: + return 'Use DHCP first' + else: + return 'Set address' + elif name == 'Hware_Hl2_EepromMAC': + addr1 = self.ReadEEPROM(0x0C) + addr2 = self.ReadEEPROM(0x0D) + if addr1 < 0 or addr2 < 0: + return "Read failed" + else: + return "0x%X 0x%X" % (addr1, addr2) + elif name == 'Hware_Hl2_EepromMACUse': + use = self.ReadEEPROM(0x06) + if use < 0: + return "Read failed" + if use & 0b1000000: + return 'Set address' + else: + return 'Ignore' + return "Name failed" + def SetValue(self, ctrl): + name = ctrl.quisk_data_name + value = ctrl.GetValue() + if name == 'Hware_Hl2_EepromIP': + try: + addr1, addr2, addr3, addr4 = value.split('.') + addr1 = int(addr1) + addr2 = int(addr2) + addr3 = int(addr3) + addr4 = int(addr4) + except: + pass + else: + self.WriteEEPROM(0x08, addr1) + self.WriteEEPROM(0x09, addr2) + self.WriteEEPROM(0x0A, addr3) + self.WriteEEPROM(0x0B, addr4) + elif name == 'Hware_Hl2_EepromIPUse': + use = self.ReadEEPROM(0x06) + if use >= 0: + self.eeprom_valid = use + if value == 'Ignore': + self.eeprom_valid &= ~0b0010000000 + elif value == 'Use DHCP first': + self.eeprom_valid |= 0b0010100000 + elif value == 'Set address': + self.eeprom_valid |= 0b0010000000 + self.eeprom_valid &= ~0b0000100000 + self.WriteEEPROM(0x06, self.eeprom_valid) + elif name == 'Hware_Hl2_EepromMAC': + try: + addr1, addr2 = value.split() + addr1 = int(addr1, base=0) + addr2 = int(addr2, base=0) + except: + pass + else: + self.WriteEEPROM(0x0C, addr1) + self.WriteEEPROM(0x0D, addr2) + elif name == 'Hware_Hl2_EepromMACUse': + use = self.ReadEEPROM(0x06) + if use >= 0: + self.eeprom_valid = use + if value == 'Ignore': + self.eeprom_valid &= ~0b0001000000 + elif value == 'Set address': + self.eeprom_valid |= 0b0001000000 + self.WriteEEPROM(0x06, self.eeprom_valid) + def GetControlByte(self, C0_index, byte_index): + # Get the control byte at C0 index and byte index. The bytes are C0, C1, C2, C3, C4. + # The C0 index is 0 to 16 inclusive. The byte index is 1 to 4. The byte index of C2 is 2. + return self.pc2hermes[C0_index * 4 + byte_index - 1] + def SetControlByte(self, C0_index, byte_index, value, prnt=True): # Set the control byte as above. + self.pc2hermes[C0_index * 4 + byte_index - 1] = value + QS.pc_to_hermes(self.pc2hermes) + if DEBUG and prnt: print ("SetControlByte C0_index 0x%X byte_index %d to 0x%X" % (C0_index, byte_index, value)) + def GetControlBit(self, C0_index, bit): + # Get the control bit (return 0 or 1) at C0 index and bit number 0 through 31. + byte_index = 4 - bit // 8 + byte_value = self.pc2hermes[C0_index * 4 + byte_index - 1] + mask = 0x01 << (bit % 8) + if byte_value & mask: + return 1 + return 0 + def SetControlBit(self, C0_index, bit, bit_value): + # Set the control bit at C0 index and bit number 0 through 31 to value (0 or 1). + byte_index = 4 - bit // 8 + byte_value = self.pc2hermes[C0_index * 4 + byte_index - 1] + mask = 0x01 << (bit % 8) + if bit_value: # set bit to one + byte_value |= mask + else: # set bit to zero + byte_value &= ~mask + self.pc2hermes[C0_index * 4 + byte_index - 1] = byte_value + QS.pc_to_hermes(self.pc2hermes) + #if DEBUG: print ("SetControlBit C0_index 0x%X byte_index %d to 0x%X" % (C0_index, byte_index, byte_value)) + def ChangeFrequency(self, tx_freq, vfo_freq, source='', band='', event=None): + if tx_freq and tx_freq > 0: + self.tx_frequency = tx_freq + self.io_board.HeartBeat() + tx = int(tx_freq - self.transverter_offset) + self.pc2hermes[ 4] = tx >> 24 & 0xff # C0 index == 1, C1, C2, C3, C4: Tx freq, MSB in C1 + self.pc2hermes[ 5] = tx >> 16 & 0xff + self.pc2hermes[ 6] = tx >> 8 & 0xff + self.pc2hermes[ 7] = tx & 0xff + if self.vfo_frequency != vfo_freq: + self.vfo_frequency = vfo_freq + vfo = int(vfo_freq - self.transverter_offset) + self.pc2hermes[ 8] = vfo >> 24 & 0xff # C0 index == 2, C1, C2, C3, C4: Rx freq, MSB in C1 + self.pc2hermes[ 9] = vfo >> 16 & 0xff + self.pc2hermes[10] = vfo >> 8 & 0xff + self.pc2hermes[11] = vfo & 0xff + if DEBUG > 1: print("Change freq Tx", tx_freq, "Rx", vfo_freq) + QS.pc_to_hermes(self.pc2hermes) + return tx_freq, vfo_freq + def Freq2Phase(self, freq=None): # Return the phase increment as calculated by the FPGA + # This code attempts to duplicate the calculation of phase increment in the FPGA code. + clock = ((int(self.conf.rx_udp_clock) + 24000) // 48000) * 48000 # this assumes the nominal clock is a multiple of 48kHz + M2 = 2 ** 57 // clock + M3 = 2 ** 24 + if freq is None: + freqcomp = int(self.vfo_frequency - self.transverter_offset) * M2 + M3 + else: + freqcomp = int(freq) * M2 + M3 + phase = (freqcomp // 2 ** 25) & 0xFFFFFFFF + return phase + def ReturnVfoFloat(self, freq=None): # Return the accurate VFO as a float + phase = self.Freq2Phase(freq) + freq = float(phase) * self.conf.rx_udp_clock / 2.0**32 + return freq + def ReturnFrequency(self): # Return the current tuning and VFO frequency + return None, None # frequencies have not changed + def HeartBeat(self): + self.TFRC_counter += 1 + key_down = QS.is_key_down() + if key_down and not self.key_was_down: # reset on key down + self.TFRC_counter = 0 + QS.get_hermes_TFRC() + self.key_was_down = key_down + if self.TFRC_counter >= 3: + self.TFRC_counter = 0 + self.hermes_temperature, self.hermes_fwd_power, self.hermes_rev_power, self.hermes_pa_current, self.hermes_fwd_peak, self.hermes_rev_peak = QS.get_hermes_TFRC() + if self.application.bottom_widgets: + self.application.bottom_widgets.UpdateText() + if self.delay_config and QS.get_params('rx_udp_started'): + self.delay_config = False + for name in ('hermes_disable_watchdog', 'hermes_tx_buffer_latency', 'hermes_reset_on_disconnect', 'hermes_iob_rxin'): + self.ImmediateChange(name) + self.io_board.HeartBeat() + def RepeaterOffset(self, offset=None): # Change frequency for repeater offset during Tx + if offset is None: # Return True if frequency change is complete + if time.time() > self.repeater_time0 + self.repeater_delay: + return True + elif offset == 0: # Change back to the original frequency + if self.repeater_freq is not None: + self.repeater_time0 = time.time() + self.ChangeFrequency(self.repeater_freq, self.vfo_frequency, 'repeater') + self.repeater_freq = None + else: # Shift to repeater input frequency + self.repeater_freq = self.tx_frequency + offset = int(offset * 1000) # Convert kHz to Hz + self.repeater_time0 = time.time() + self.ChangeFrequency(self.tx_frequency + offset, self.vfo_frequency, 'repeater') + return False + def ChangeBand(self, band): + # band is a string: "60", "40", "WWV", etc. + BaseHardware.ChangeBand(self, band) + self.band = band + self.ChangeBandFilters() + self.SetTxLevel() + def OnButtonAntenna(self, event): + btn = event.GetEventObject() + self.antenna_index = btn.index + self.io_board.Antenna(self.antenna_index, self.antenna_index) + self.ChangeBandFilters() + def ChangeBandFilters(self): + if not hasattr(self.application, "multi_rx_screen"): + return # Needed for the VNA program + self.SetControlBit(0x00, 13, self.antenna_index) + highest = self.band + freq = self.conf.BandEdge.get(highest, (0, 0))[0] + for pane in self.application.multi_rx_screen.receiver_list: + f = self.conf.BandEdge.get(pane.band, (0, 0))[0] + if freq < f: + freq = f + highest = pane.band + # Set Hermes Rx and Tx filters for C0 == 0x00, C2[7:1] + Rx = self.conf.Hermes_BandDict.get(highest, 0) + self.SetControlByte(0, 2, Rx << 1, False) # C0 index == 0, C2[7:1]: user output + if self.conf.Hermes_BandDictEnTx: + Tx = self.conf.Hermes_BandDictTx.get(self.band, 0) # Use Tx filter + else: + Tx = self.conf.Hermes_BandDict.get(self.band, 0) # Use the Rx filter for the Tx band + QS.set_hermes_filters(Rx, Tx) + if DEBUG: print("Change Hermes Band Filters: Antenna %d Rx 0x%X Tx 0x%X" % (self.antenna_index, Rx, Tx)) + # Set Alex filters for C0 == 0x09 + rx_value = Rx & 0x7F + if self.antenna_index: + rx_value |= 0x80 + self.SetControlByte(0x09, 3, rx_value, False) # C0 index == 0x09, C3, Rx filter + Tx = self.conf.Hermes_BandDictTx.get(self.band, 0) # Tx filter + tx_value = Tx & 0x7F + if self.antenna_index: + tx_value |= 0x80 + self.SetControlByte(0x09, 4, tx_value, False) # C0 index == 0x09, C4, Tx filter + if DEBUG: print("Change Alex Band Filters: Antenna %d Rx 0x%X Tx 0x%X" % (self.antenna_index, rx_value, tx_value)) + def ChangeMode(self, mode): + # mode is a string: "USB", "AM", etc. + BaseHardware.ChangeMode(self, mode) + self.mode = mode + self.SetTxLevel() + def OnSpot(self, level): + # level is -1 for Spot button Off; else the Spot level 0 to 1000. + if level < 0 or self.conf.hermes_antenna_tuner != "Tune": + self.SetControlBit(0x09, 17, 0) + self.SetControlBit(0x09, 20, 0) + if DEBUG: print("OnSpot antenna tuner: Off") + elif level == 0: + self.SetControlBit(0x09, 17, 1) # Bypass tuner if bit 17 set + self.SetControlBit(0x09, 20, 1) + if DEBUG: print("OnSpot antenna tuner: Bypass") + else: + self.SetControlBit(0x09, 17, 0) + self.SetControlBit(0x09, 20, 1) + if DEBUG: print("OnSpot antenna tuner: Tune") + def VarDecimGetChoices(self): # return text labels for the control + return self.var_rates + def VarDecimGetLabel(self): # return a text label for the control + return "Sample rate ksps" + def VarDecimGetIndex(self): # return the current index + return self.var_index + def VarDecimSet(self, index=None): # set decimation, return sample rate + if index is None: # initial call to set rate before the call to open() + rate = self.application.vardecim_set # May be None or from different hardware + else: + rate = int(self.var_rates[index]) * 1000 + if rate == 48000: + self.var_index = 0 + elif rate == 96000: + self.var_index = 1 + elif rate == 192000: + self.var_index = 2 + elif rate == 384000: + self.var_index = 3 + else: + self.var_index = 0 + rate = 48000 + self.pc2hermes[0] = self.var_index # C0 index == 0, C1[1:0]: rate + QS.pc_to_hermes(self.pc2hermes) + if DEBUG: print ("Change sample rate to", rate) + return rate + def VarDecimRange(self): + return (48000, 384000) + ## Hardware AGC is no longer supported in HL2 identifying as version >=40 + def ChangeAGC(self, value): + if value: + self.pc2hermes[2] |= 0x10 # C0 index == 0, C3[4]: AGC enable + else: + self.pc2hermes[2] &= ~0x10 + QS.pc_to_hermes(self.pc2hermes) + if DEBUG: print ("Change AGC to", value) + ## Simpler LNA setting for HL2 identifying as version >=40, see HL2 wiki for details + def ChangeLNA(self, value): # LNA for Rx + # value is -12 to +48 + if self.hermes_code_version < 40: # Is this correct ?? + if value < 20: + self.pc2hermes[2] |= 0x08 # C0 index == 0, C3[3]: LNA +32 dB disable == 1 + value = 19 - value + else: + self.pc2hermes[2] &= ~0x08 # C0 index == 0, C3[3]: LNA +32 dB enable == 0 + value = 51 - value + else: + value = ((value+12) & 0x3f) | 0x40 + self.pc2hermes[4 * 10 + 3] = value # C0 index == 0x1010, C4[4:0] LNA 0-32 dB gain + QS.pc_to_hermes(self.pc2hermes) + if DEBUG: print ("Change LNA to", value) + def ChangeTxLNA(self, value): # LNA for Tx + # value is -12 to +48 + if value < -12: + value = -12 + elif value > 48: + value = 48 + value = ((value+12) & 0x3f) | 0x40 | 0x80 + self.SetControlByte(0x0e, 3, value, False) # C0 index == 0x0e, C3 + QS.pc_to_hermes(self.pc2hermes) + if DEBUG: print ("Change Tx LNA to", value) + def SetTxLevel(self): + try: + tx_level = self.conf.tx_level[self.band] + except KeyError: + tx_level = self.conf.tx_level.get(None, 127) # The default + if self.mode[0:3] in ('DGT', 'FDV'): # Digital modes; change power by a percentage + reduc = self.application.digital_tx_level + else: + reduc = self.application.tx_level + tx_level = int(tx_level *reduc/100.0) + if tx_level < 0: + tx_level = 0 + elif tx_level > 255: + tx_level = 255 + self.pc2hermes[4 * 9] = tx_level # C0 index == 0x1001, C1[7:0] Tx level + QS.pc_to_hermes(self.pc2hermes) + if DEBUG: print("Change tx_level to", tx_level) + def MultiRxCount(self, count): # count == number of additional receivers besides the Tx/Rx receiver: 1, 2, 3 + # C0 index == 0, C4[5:3]: number of receivers 0b000 -> one receiver; C4[2] duplex on + self.pc2hermes[3] = 0x04 | count << 3 + QS.pc_to_hermes(self.pc2hermes) + if DEBUG: print("Change MultiRx count to", count) + def MultiRxFrequency(self, index, vfo, band): # index of multi rx receiver: 0, 1, 2, ... + self.io_board.NewRxFreq(index + 1, vfo) + try: + vfo -= self.conf.bandTransverterOffset[band] + except: + pass + if DEBUG: print("Change MultiRx %d frequency to %d in band %s" % (index, vfo, band)) + if index <= 5: + C0 = index + 3 + jndex = C0 * 4 + self.pc2hermes[jndex ] = vfo >> 24 & 0xff + self.pc2hermes[jndex + 1] = vfo >> 16 & 0xff # C1, C2, C3, C4: Rx freq, MSB in C1 + self.pc2hermes[jndex + 2] = vfo >> 8 & 0xff + self.pc2hermes[jndex + 3] = vfo & 0xff + elif index <= 10: + C0 = index + 12 + jndex = C0 * 4 + self.pc2hermes[jndex ] = vfo >> 24 & 0xff + self.pc2hermes[jndex + 1] = vfo >> 16 & 0xff # C1, C2, C3, C4: Rx freq, MSB in C1 + self.pc2hermes[jndex + 2] = vfo >> 8 & 0xff + self.pc2hermes[jndex + 3] = vfo & 0xff + QS.pc_to_hermes(self.pc2hermes) + def SetVNA(self, key_down=None, vna_start=None, vna_stop=None, vna_count=None, do_tx=False): + if vna_count is not None: # must be called first + if DEBUG: print("vna_count", vna_count) + self.vna_count = vna_count + if vna_start is None: + start = 0 + stop = 0 + else: # Set the start and stop frequencies and the frequency change for each point + # vna_start and vna_stop must be specified together + self.pc2hermes[ 4] = vna_start >> 24 & 0xff # C0 index == 1, C1, C2, C3, C4: Tx freq, MSB in C1 + self.pc2hermes[ 5] = vna_start >> 16 & 0xff # used for vna starting frequency + self.pc2hermes[ 6] = vna_start >> 8 & 0xff + self.pc2hermes[ 7] = vna_start & 0xff + N = self.vna_count - 1 + ph_start = self.Freq2Phase(vna_start) # Calculate using phases + ph_stop = self.Freq2Phase(vna_stop) + delta = (ph_stop - ph_start + N // 2) // N + delta = int(float(delta) * self.conf.rx_udp_clock / 2.0**32 + 0.5) + self.pc2hermes[ 8] = delta >> 24 & 0xff # C0 index == 2, C1, C2, C3, C4: Rx freq, MSB in C1 + self.pc2hermes[ 9] = delta >> 16 & 0xff # used for the frequency to add for each point + self.pc2hermes[10] = delta >> 8 & 0xff + self.pc2hermes[11] = delta & 0xff + self.pc2hermes[4 * 9 + 2] = (self.vna_count >> 8) & 0xff # C0 index == 0b1001, C3 + self.pc2hermes[4 * 9 + 3] = self.vna_count & 0xff # C0 index == 0b1001, C4 + QS.pc_to_hermes(self.pc2hermes) + start = self.ReturnVfoFloat(vna_start) + phase = ph_start + self.Freq2Phase(delta) * N + stop = float(phase) * self.conf.rx_udp_clock / 2.0**32 + start = int(start + 0.5) + stop = int(stop + 0.5) + if DEBUG: print ("Change VNA start", vna_start, start, "stop", vna_stop, stop, 'count', self.vna_count) + if key_down is None: + pass + elif key_down: + if not self.vna_started: + self.vna_started = True + self.SetControlByte(9, 2, 0x80) # turn on VNA mode + if DEBUG: print("vna_started TRUE") + QS.set_key_down(1) + if DEBUG: print ("vna key down") + else: + QS.set_key_down(0) + if DEBUG: print ("vna key up") + return start, stop # Return actual frequencies after all phase rounding + def ImmediateChange(self, name): + if name == 'keyupDelay': + value = self.conf.keyupDelay + if value > 1023: + value = 1023 + self.SetControlByte(0x10, 2, value & 0x3, False) # cw_hang_time + self.SetControlByte(0x10, 1, (value >> 2) & 0xFF, False) # cw_hang_time + if DEBUG: print ("Change keyup delay to", value) + elif name in ('hermes_tx_buffer_latency', 'hermes_PTT_hang_time'): + lat = self.conf.hermes_tx_buffer_latency + if lat < 0: + lat = 0 + elif lat > 127: + lat = 127 + hang = self.conf.hermes_PTT_hang_time + if hang < 0: + hang = 0 + elif hang > 31: + hang = 31 + self.pc2hermeslitewritequeue[0:5] = 0x17 | 0x40, 0, 0, hang, lat + self.WriteQueue() + if DEBUG: print ("WriteQueue: Change tx_buffer_latency %d, PTT_hang_time %d" % (lat, hang)) + elif name == 'hermes_PWM': + if self.conf.hermes_PWM[0:4] == 'Fan ': + self.SetControlBit(0x00, 11, 0) + else: + self.SetControlBit(0x00, 11, 1) + if DEBUG: print ("Change hermes_PWM to", self.conf.hermes_PWM) + elif name == 'hermes_disable_sync': + if self.conf.hermes_disable_sync: + self.SetControlBit(0x00, 12, 1) + if DEBUG: print ("Set hermes_disable_sync") + else: + self.SetControlBit(0x00, 12, 0) + if DEBUG: print ("Clear hermes_disable_sync") + elif name == 'hermes_disable_watchdog': + if self.conf.hermes_disable_watchdog: + self.pc2hermeslitewritequeue[0:5] = 0x39 | 0x40, 0x09, 0, 0, 0 + self.WriteQueue() + if DEBUG: print ("WriteQueue: Set hermes_disable_watchdog") + else: + self.pc2hermeslitewritequeue[0:5] = 0x39 | 0x40, 0x08, 0, 0, 0 + self.WriteQueue() + if DEBUG: print ("WriteQueue: Clear hermes_disable_watchdog") + elif name == 'hermes_reset_on_disconnect': + if self.conf.hermes_reset_on_disconnect: + self.pc2hermeslitewritequeue[0:5] = 0x3A | 0x40, 0, 0, 0, 0x01 + self.WriteQueue() + if DEBUG: print ("WriteQueue: Set hermes_reset_on_disconnect") + else: + self.pc2hermeslitewritequeue[0:5] = 0x3A | 0x40, 0, 0, 0, 0 + self.WriteQueue() + if DEBUG: print ("WriteQueue: Clear hermes_reset_on_disconnect") + elif name == 'hermes_lowpwr_tr_enable': + if self.conf.hermes_lowpwr_tr_enable: + self.SetControlBit(0x09, 18, 1) + else: + self.SetControlBit(0x09, 18, 0) + if DEBUG: print ("Change disable T/R in low power to", self.conf.hermes_lowpwr_tr_enable) + elif name == 'hermes_power_amp': + if self.conf.hermes_power_amp: + self.SetControlBit(0x09, 19, 1) + else: + self.SetControlBit(0x09, 19, 0) + if DEBUG: print ("Change power_amp to", self.conf.hermes_power_amp) + elif name == 'Hermes_BandDictEnTx': + if self.conf.Hermes_BandDictEnTx: + self.SetControlBit(0x09, 22, 1) + else: + self.SetControlBit(0x09, 22, 0) + if DEBUG: print ("Change Alex manual mode to", self.conf.Hermes_BandDictEnTx) + self.ChangeBandFilters() + elif name == 'hermes_antenna_tuner': + pass + elif name == 'hermes_iob_rxin': + mode = self.conf.hermes_iob_rxin[0:4] + if mode == 'J10 ': + self.io_board.AuxRxInput(0) + elif mode == 'HL2 ': + self.io_board.AuxRxInput(1) + elif mode == 'Use ': + self.io_board.AuxRxInput(2) + else: + if DEBUG: print ("Immediate change: no such name", name) + def EnableBiasChange(self, enable): + # Bias settings are in location 12, 13, 14, 15, and are not sent unless C1 == 0x06 + if enable: + for base in (12, 13, 14, 15): + self.pc2hermes[4 * base] = 0x06 # C1 + self.pc2hermes[4 * base + 1] = 0xA8 # C2 + self.pc2hermes[4 * 12 + 2] = 0x00 # C3 bias 1, volitile + self.pc2hermes[4 * 13 + 2] = 0x20 # C3 bias 1, non-volitile + self.pc2hermes[4 * 14 + 2] = 0x10 # C3 bias 2, volitile + self.pc2hermes[4 * 15 + 2] = 0x30 # C3 bias 2, non-volitile + else: + for base in (12, 13, 14, 15): + self.pc2hermes[4 * base] = 0x00 # C1 + QS.pc_to_hermes(self.pc2hermes) + if DEBUG: print ("Enable bias change", enable) + ## Bias is 0 indexed to match schematic + ## Changes for HermesLite v2 thanks to Steve, KF7O + def ChangeBias0(self, value): + if self.hermes_code_version >= 60: + i2caddr,value = 0xac,(value%256) + else: + i2caddr,value = 0xa8,(255-(value%256)) + self.pc2hermeslitewritequeue[0:5] = 0x7d,0x06,i2caddr,0x00,value + self.WriteQueue() + if DEBUG: print ("Change bias 0", value) + def ChangeBias1(self, value): + if self.hermes_code_version >= 60: + i2caddr,value = 0xac,(value%256) + else: + i2caddr,value = 0xa8,(255-(value%256)) + self.pc2hermeslitewritequeue[0:5] = 0x7d,0x06,i2caddr,0x10,value + self.WriteQueue() + if DEBUG: print ("Change bias 1", value) + def WriteBias(self, value0, value1): + if self.hermes_code_version >= 60: + i2caddr,value0 = 0xac,(value0%256) + else: + i2caddr,value0 = 0xa8,(255-(value0%256)) + self.pc2hermeslitewritequeue[0:5] = 0x7d,0x06,i2caddr,0x20,value0 + self.WriteQueue() + ## Wait >10ms as that is the longest EEPROM write cycle time + time.sleep(0.015) + value1 = (value1%256) if self.hermes_code_version >= 60 else (255-(value1%256)) + self.pc2hermeslitewritequeue[0:5] = 0x7d,0x06,i2caddr,0x30,value1 + self.WriteQueue() + ## Double write bias to EEPROM + time.sleep(0.030) + self.pc2hermeslitewritequeue[0:5] = 0x7d,0x06,i2caddr,0x30,value1 + self.WriteQueue() + time.sleep(0.015) + self.pc2hermeslitewritequeue[0:5] = 0x7d,0x06,i2caddr,0x20,value0 + self.WriteQueue() + if DEBUG: print ("Write bias", value0, value1) + def _wait_queue(self): + # Wait for the write to finish + for i in range(50): + wp = QS.get_hermeslite_writepointer() + if wp == 0: + return True + time.sleep(0.010) + else: + print("ERROR: Hermes-Lite write queue timeout, queue 0x%X 0x%X 0x%X 0x%X 0x%X" % tuple(self.old_writequeue)) + QS.set_hermeslite_writepointer(0) + return False + def WriteQueue(self, wait=False): + self._wait_queue() + # Send next write + self.old_writequeue = self.pc2hermeslitewritequeue[:] + QS.pc_to_hermeslite_writequeue(self.pc2hermeslitewritequeue) + QS.set_hermeslite_writepointer(1) + if DEBUG: print("Hermes-Lite write queue request, queue 0x%X 0x%X 0x%X 0x%X 0x%X" % tuple(self.pc2hermeslitewritequeue)) + if wait: + # Wait for the write to complete + return self._wait_queue() + return True + ## In HL2 firmware identifying as version >=40, AD9866 access is available + ## See AD9866 datasheet for details, some examples: + ## self.writeAD9866(0x08,0xff) ## Set LPF target frequency + ## self.WriteAD9866(0x07,0x01) ## Enable RX LPF, RX to high power usage + ## self.WriteAD9866(0x07,0x00) ## Disable RX LPF, RX to high power usage + ## self.WriteAD9866(0x0e,0x81) ## Low digital drive strength + ## self.WriteAD9866(0x0e,0x01) ## High digital drive strength + ## Set RX bias to default levels + ## cpga = 0 + ## spga = 0 + ## adcb = 0 + ## self.WriteAD9866(0x13,((cpga & 0x07) << 5) | ((spga & 0x03) << 3) | (adcb & 0x07)) + def WriteAD9866(self,addr,data): + addr = addr & 0x01f + data = data & 0x0ff + self.pc2hermeslitewritequeue[0:5] = 0x7b,0x06,addr,0x00,data + self.WriteQueue() + if DEBUG: print ("Write AD9866 addr={0:06x} data={1:06x}".format(addr,data)) + def MakePowerCalibration(self): + # Use spline interpolation to convert the ADC power sensor value to power in watts + name = self.conf.power_meter_calib_name + try: # look in config file + table = self.conf.power_meter_std_calibrations[name] + except: + try: # look in local name space + table = self.application.local_conf.GetRadioDict().get('power_meter_local_calibrations', {})[name] + except: # not found + self.power_interpolator = None + return + if len(table) < 3: + self.power_interpolator = None + return + table.sort() + if table[0][0] > 0: # Add zero code at zero power + table.insert(0, [0, 0.0]) + # fill out the table to the maximum code 4095 + l = len(table) - 1 + x = table[l][0] * 1.1 # voltage increase + y = table[l][1] * 1.1**2 # square law power increase + while 1: + table.append([x, y]) + if x > 4095: + break + x *= 1.1 + y *= 1.1**2 + self.power_interpolator = quisk_utils.SplineInterpolator(table) + def InterpolatePower(self, x): + if not self.power_interpolator: + return 0.0 + y = self.power_interpolator.Interpolate(x) + if y < 0.0: + y = 0.0 + return y + def VersaOut2(self, divisor): # Use the VersaClock output 2 with a floating point divisor + div = int(divisor * 2**24 + 0.1) + intgr = div >> 24 + frac = (div & 0xFFFFFF) << 2 + self.WriteVersa5(0x62,0x3b) # Clock2 CMOS1 output, 3.3V + self.WriteVersa5(0x2c,0x00) # Disable aux output on clock 1 + self.WriteVersa5(0x31,0x81) # Use divider for clock2 + # Integer portion + self.WriteVersa5(0x3d, intgr >> 4) + self.WriteVersa5(0x3e, intgr << 4) + # Fractional portion + self.WriteVersa5(0x32,frac >> 24) # [29:22] + self.WriteVersa5(0x33,frac >> 16) # [21:14] + self.WriteVersa5(0x34,frac >> 8) # [13:6] + self.WriteVersa5(0x35,(frac & 0xFF)<<2) # [5:0] and disable ss + self.WriteVersa5(0x63,0x01) # Enable clock2 + # Thanks to Steve Haynal for VersaClock code: + def WriteVersa5(self,addr,data): + data = data & 0x0ff + addr = addr & 0x0ff + ## i2caddr is 7 bits, no read write + ## Bit 8 is set to indicate stop to HL2 + ## i2caddr = 0x80 | (0xd4 >> 1) ## ea + self.pc2hermeslitewritequeue[0:5] = 0x7c,0x06,0xea,addr,data + self.WriteQueue() + def EnableCL2_sync76p8MHz(self): + self.WriteVersa5(0x62,0x3b) ## Clock2 CMOS1 output, 3.3V + self.WriteVersa5(0x2c,0x01) ## Enable aux output on clock 1 + self.WriteVersa5(0x31,0x0c) ## Use clock1 aux output as input for clock2 + self.WriteVersa5(0x63,0x01) ## Enable clock2 + def EnableCL2_61p44MHz(self): + self.WriteVersa5(0x62,0x3b) ## Clock2 CMOS1 output, 3.3V + self.WriteVersa5(0x2c,0x00) ## Disable aux output on clock 1 + self.WriteVersa5(0x31,0x81) ## Use divider for clock2 + ## VCO multiplier is shared for all outputs, set to 68 by firmware + ## VCO = 38.4*68 = 2611.2 MHz + ## There is a hardwired divide by 2 in the Versa 5 at the VCO output + ## VCO to Dividers = 2611.2 MHZ/2 = 1305.6 + ## Target frequency of 61.44 requires dividers of 1305.6/61.44 = 21.25 + ## Frational dividers are supported + ## Set integer portion of divider 21 = 0x15, 12 bits split across 2 registers + self.WriteVersa5(0x3d,0x01) + self.WriteVersa5(0x3e,0x50) + ## Set fractional portion, 30 bits, 2**24 * .25 = 0x400000 + self.WriteVersa5(0x32,0x01) ## [29:22] + self.WriteVersa5(0x33,0x00) ## [21:14] + self.WriteVersa5(0x34,0x00) ## [13:6] + self.WriteVersa5(0x35,0x00) ## [5:0] and disable ss + self.WriteVersa5(0x63,0x01) ## Enable clock2 + def WriteEEPROM(self, addr, value): + ## Write values into the MCP4662 EEPROM registers + ## For example, to set a fixed IP of 192.168.33.20 + ## hw.WriteEEPROM(8,192) + ## hw.WriteEEPROM(9,168) + ## hw.WriteEEPROM(10,33) + ## hw.WriteEEPROM(11,20) + ## To set the last two values of the MAC to 55:66 + ## hw.WriteEEPROM(12,55) + ## hw.WriteEEPROM(13,66) + ## To enable the fixed IP and alternate MAC, and favor DHCP + ## hw.WriteEEPROM(6, 0x80 | 0x40 | 0x20) + ## See https://github.com/softerhardware/Hermes-Lite2/wiki/Protocol + if self.hermes_code_version >= 60: + i2caddr,value = 0xac,(value%256) + else: + i2caddr,value = 0xa8,(255-(value%256)) + addr = (addr << 4)%256 + self.pc2hermeslitewritequeue[0:5] = 0x7d,0x06,i2caddr,addr,value + self.WriteQueue() + if DEBUG: print ("Write EEPROM", addr, value) + def ReadEEPROM(self, addr): + ## To read the bias settings for bias0 and bias1 + ## hw.ReadEEPROM(2) + ## hw.ReadEEPROM(3) + if self.hermes_code_version >= 60: + i2caddr = 0xac + else: + i2caddr = 0xa8 + faddr = ((addr << 4)%256) | 0xc + QS.clear_hermeslite_response() + self.pc2hermeslitewritequeue[0:5] = 0x7d,0x07,i2caddr,faddr,0 + self.WriteQueue() + for j in range(50): + time.sleep(0.001) + resp = QS.get_hermeslite_response() + ##print("RESP:",j,resp[0],resp[1],resp[2],resp[3],resp[4]) + if resp[0] != 0: break + if resp[0] == 0: + if DEBUG: print("EEPROM read did not return a value") + return -1 + else: + ## MCP4662 does not autoincrement when reading 8 bytes + ## MCP4662 stores 9 bit values, msb came first and is in lower order byte + v0 = (resp[4] << 8) | resp[3] + v1 = (resp[2] << 8) | resp[1] + if (resp[0] >> 1) != 0x7d: + ## Response mismatch + if DEBUG: print("EEPROM read response mismatch",resp[0] >> 1) + return -1 + elif v0 != v1: + if DEBUG: print("EEPROM read values do not agree",v0,v1) + return -1 + else: + if DEBUG: print("EEPROM read {0:#x} from address {1:#x}".format(v0,addr)) + return v0 + def WriteI2C(self, bus, i2caddr, control, value): + # bus is 0x7c or 0x7d + self.pc2hermeslitewritequeue[0:5] = bus, 0x06, i2caddr, control, value + QS.clear_hermeslite_response() + if self.WriteQueue(wait=True): + if DEBUG_I2C > 1 or DEBUG: print ("Write I2C bus 0x%X, i2caddr 0x%X, control 0x%X, value 0x%X" % (bus, i2caddr, control, value)) + else: + if DEBUG_I2C or DEBUG: + print ("Write I2C bus ERROR 0x%X, i2caddr 0x%X, control 0x%X, value 0x%X" % (bus, i2caddr, control, value)) + def ReadI2C(self, bus, i2caddr, control): + # bus is 0x7c or 0x7d + # Beware of byte order! + self.pc2hermeslitewritequeue[0:5] = bus, 0x07, i2caddr, control, 0 + QS.clear_hermeslite_response() + if self.WriteQueue(wait=True): + resp = QS.get_hermeslite_response() + resp[0] = (resp[0] >> 1) & 0x3F # 6-bit bus in C0 + if DEBUG_I2C or DEBUG: + print ("Read I2C bus 0x%X, 0x%X, 0x%X, 0x%X, 0x%X " % tuple(resp)) + return resp + else: + if DEBUG_I2C or DEBUG: + print("ReadI2C timed out and did not return a value") + return None + def ProgramGateware(self, event): # Program the Gateware (FPGA firmware) over Ethernet + title = "Program the Gateware" + main_frame = self.application.main_frame + dlg = wx.FileDialog(main_frame, message='Choose an RBF file for programming the Gateware', + style=wx.FD_OPEN, wildcard="RBF files (*.rbf)|*.rbf") + if dlg.ShowModal() == wx.ID_OK: + path = dlg.GetPath() + dlg.Destroy() + else: + dlg.Destroy() + return + timeout = 0.2 # socket timeout in seconds + erase_time = 50 # in units of timeout + hermes_ip = self.hermes_ip + hermes_mac = self.hermes_mac + if not hermes_ip: + msg = wx.MessageDialog(main_frame, "No Hermes hardware was found.", title, wx.OK|wx.ICON_ERROR) + msg.ShowModal() + msg.Destroy() + return + try: + fp = open(path, "rb") + size = os.stat(path).st_size + except: + msg = wx.MessageDialog(main_frame, "Can not read the RBF file specified.", title, wx.OK|wx.ICON_ERROR) + msg.ShowModal() + msg.Destroy() + return + for i in range(10): + state = QS.set_params(hermes_pause=1) + #print ("state", state) + if state == 23: + break + else: + time.sleep(0.05) + else: + msg = wx.MessageDialog(main_frame, "Failure to find a running Hermes and stop the samples.", title, wx.OK|wx.ICON_ERROR) + msg.ShowModal() + msg.Destroy() + fp.close() + return + blocks = (size + 255) // 256 + dlg = wx.ProgressDialog(title, "Erase old program...", blocks + 1, main_frame, wx.PD_APP_MODAL) + program_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + program_socket.settimeout(timeout) + port = self.conf.rx_udp_port + program_socket.connect((hermes_ip, port)) + cmd = bytearray(64) # Erase command + cmd[0] = 0xEF + cmd[1] = 0xFE + cmd[2] = 0x03 + cmd[3] = 0x02 + program_socket.send(cmd) + success = False + for i in range(erase_time): + dlg.Update(i * blocks // erase_time) + try: + reply = program_socket.recv(1500) + except socket.timeout: + pass + else: + reply = bytearray(reply) + if reply[0:3] == bytearray(b"\xEF\xFE\03") and reply[3:9] == hermes_mac: + success = True + break + if not success: + dlg.Destroy() + self.application.Yield() + fp.close() + msg = wx.MessageDialog(main_frame, "Failure to erase the old program. Please push the Program button again.", title, wx.OK|wx.ICON_ERROR) + msg.ShowModal() + msg.Destroy() + program_socket.close() + return + dlg.Update(0, "Programming...") + cmd = bytearray(8) + cmd[0] = 0xEF + cmd[1] = 0xFE + cmd[2] = 0x03 + cmd[3] = 0x01 + cmd[4] = (blocks >> 24) & 0xFF + cmd[5] = (blocks >> 16) & 0xFF + cmd[6] = (blocks >> 8) & 0xFF + cmd[7] = (blocks ) & 0xFF + for block in range(blocks): + dlg.Update(block) + prog = fp.read(256) + if block == blocks - 1: # last block may have an odd number of bytes + prog = prog + bytearray(b"\xFF" * (256 - len(prog))) + if len(prog) != 256: + print ("read wrong number of bytes for block", block) + success = False + break + try: + program_socket.send(cmd + prog) + reply = program_socket.recv(1500) + except socket.timeout: + print ("Socket timeout while programming block", block) + success = False + break + else: + reply = bytearray(reply) + if reply[0:3] != bytearray(b"\xEF\xFE\04") or reply[3:9] != hermes_mac: + print ("Program failed at block", block) + success = False + break + fp.close() + for i in range(10): # throw away extra packets + try: + program_socket.recv(1500) + except socket.timeout: + break + if success: + dlg.Update(0, "Waiting for Hermes to start...") + wait_secs = 15 # number of seconds to wait for restart + cmd = bytearray(63) # Discover + cmd[0] = 0xEF + cmd[1] = 0xFE + cmd[2] = 0x02 + program_socket.settimeout(1.0) + for i in range(wait_secs): + dlg.Update(i * blocks // wait_secs) + if i < 5: + time.sleep(1.0) + continue + program_socket.send(cmd) + try: + reply = program_socket.recv(1500) + except socket.timeout: + pass + else: + reply = bytearray(reply) + #print ("0x%X 0x%X %d 0x%X 0x%X 0x%X 0x%X 0x%X 0x%X %d %d" % tuple(reply[0:11])) + if reply[0] == 0xEF and reply[1] == 0xFE and reply[10] == 6: + self.hermes_mac = reply[3:9] + self.hermes_code_version = reply[9] + st = 'Capture from Hermes device: Mac %2x:%2x:%2x:%2x:%2x:%2x, Code version %d, ID %d' % tuple(reply[3:11]) + st += ', IP %s' % self.hermes_ip + self.config_text = st + #print (st) + self.application.config_text = st + self.application.main_frame.SetConfigText(st) + QS.set_params(hermes_pause=0) + break + dlg.Destroy() + self.application.Yield() + else: + dlg.Destroy() + self.application.Yield() + msg = wx.MessageDialog(main_frame, "Programming failed. Please push the Program button again.", title, wx.OK|wx.ICON_ERROR) + msg.ShowModal() + msg.Destroy() + program_socket.close() diff --git a/hermes/quisk_widgets.py b/hermes/quisk_widgets.py new file mode 100644 index 0000000..8655f15 --- /dev/null +++ b/hermes/quisk_widgets.py @@ -0,0 +1,203 @@ +# Please do not change this widgets module for Quisk. Instead copy +# it to your own quisk_widgets.py and make changes there. +# +# This module is used to add extra widgets to the QUISK screen. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import math, wx, time +from configure import ComboCtrl + + +class BottomWidgets: # Add extra widgets to the bottom of the screen + REG_ANTENNA_TUNER = 7 + def __init__(self, app, hardware, conf, frame, gbs, vertBox): + self.config = conf + self.hardware = hardware + self.application = app + self.start_row = app.widget_row # The first available row + self.start_col = app.button_start_col # The start of the button columns + self.tuner_time = 0 + self.Widgets_0x06(app, hardware, conf, frame, gbs, vertBox) + def Widgets_0x06(self, app, hardware, conf, frame, gbs, vertBox): + self.num_rows_added = 1 + start_row = self.start_row + self.atu_ctrl = ComboCtrl(frame, "ATU", ["Tune", "Bypass"], True) + gbs.Add(self.atu_ctrl, (start_row, self.start_col), (1, 2), flag=wx.EXPAND) + bw, bh = self.atu_ctrl.GetMinSize() + frame.Bind(wx.EVT_COMBOBOX_CLOSEUP, self.OnAtu) + init = app.hermes_LNA_dB + self.sliderLNA = app.SliderBoxHH(frame, 'RfLna %d dB', init, -12, 48, self.OnLNA, True) + self.sliderLNA.idName = "RfLna" + app.midiControls["RfLna"] = (self.sliderLNA, self.OnLNA) + hardware.ChangeLNA(init) + gbs.Add(self.sliderLNA, (start_row, self.start_col + 2), (1, 8), flag=wx.EXPAND) + if conf.button_layout == "Small screen": + # Display four data items in a single window + self.text_temperature = app.QuiskText1(frame, '', bh) + self.text_pa_current = app.QuiskText1(frame, '', bh) + self.text_fwd_power = app.QuiskText1(frame, '', bh) + self.text_swr = app.QuiskText1(frame, '', bh) + self.text_data = self.text_temperature + self.text_pa_current.Hide() + self.text_fwd_power.Hide() + self.text_swr.Hide() + b = app.QuiskPushbutton(frame, self.OnTextDataMenu, '..') + szr = self.data_sizer = wx.BoxSizer(wx.HORIZONTAL) + szr.Add(self.text_data, 1, flag=wx.ALIGN_CENTER_VERTICAL) + szr.Add(b, 0, flag=wx.ALIGN_CENTER_VERTICAL) + gbs.Add(szr, (start_row, self.start_col + 10), (1, 2), flag=wx.EXPAND) + # Make a popup menu for the data window + self.text_data_menu = wx.Menu() + item = self.text_data_menu.Append(-1, 'Temperature') + app.Bind(wx.EVT_MENU, self.OnDataTemperature, item) + item = self.text_data_menu.Append(-1, 'PA Current') + app.Bind(wx.EVT_MENU, self.OnDataPaCurrent, item) + item = self.text_data_menu.Append(-1, 'Fwd Power') + app.Bind(wx.EVT_MENU, self.OnDataFwdPower, item) + item = self.text_data_menu.Append(-1, 'SWR') + app.Bind(wx.EVT_MENU, self.OnDataSwr, item) + else: + szr = wx.BoxSizer(wx.HORIZONTAL) + gbs.Add(szr, (start_row, self.start_col + 10), (1, 18), flag=wx.EXPAND) + text_temperature = wx.StaticText(frame, -1, ' Temp 100DC XX', style=wx.ST_NO_AUTORESIZE) + size = text_temperature.GetBestSize() + text_temperature.Destroy() + self.text_temperature = wx.StaticText(frame, -1, '', size=size, style=wx.ST_NO_AUTORESIZE) + self.text_pa_current = wx.StaticText(frame, -1, '', size=size, style=wx.ST_NO_AUTORESIZE) + self.text_fwd_power = wx.StaticText(frame, -1, '', size=size, style=wx.ST_NO_AUTORESIZE) + self.text_swr = wx.StaticText(frame, -1, '', size=size, style=wx.ST_NO_AUTORESIZE) + flag=wx.ALIGN_LEFT|wx.ALIGN_CENTER_VERTICAL + szr.Add(self.text_temperature, 0, flag=flag) + szr.Add(self.text_pa_current, 0, flag=flag) + szr.Add(self.text_fwd_power, 0, flag=flag) + szr.Add(self.text_swr, 0, flag=flag) + def OnAtu(self, event): + if not self.hardware.io_board.have_IO_Board: + self.atu_ctrl.SetText("No ATU") + self.tuner_time = 0 + elif self.atu_ctrl.GetValue() == "Tune": + self.atu_ctrl.SetText("Tuning") + self.tuner_time = time.time() + self.hardware.WriteI2C(0x7d, 0x1D, self.REG_ANTENNA_TUNER, 1) + else: + self.atu_ctrl.SetText("Bypass") + self.tuner_time = 0 + self.hardware.WriteI2C(0x7d, 0x1D, self.REG_ANTENNA_TUNER, 2) + if self.application.spotButton.GetValue(): + self.application.spotButton.SetValue(False, True) + self.sliderLNA.SetFocus() + def OnLNA(self, event=None): + value = self.sliderLNA.GetValue() + self.hardware.ChangeLNA(value) + self.application.hermes_LNA_dB = value + def Code2Temp(self): # Convert the HermesLite temperature code to the temperature + temp = self.hardware.hermes_temperature + # For best accuracy, 3.26 should be a user's measured 3.3V supply voltage. + temp = (3.26 * (temp/4096.0) - 0.5)/0.01 + return temp + def Code2Current(self): # Convert the HermesLite PA current code to amps + current = self.hardware.hermes_pa_current + # 3.26 Ref voltage + # 4096 steps in ADC + # Gain of x50 for sense amp + # Sense resistor is 0.04 Ohms + current = ((3.26 * (current/4096.0))/50.0)/0.04 + # Scale by resistor voltage divider 1000/(1000+270) at input of slow ADC + current = current / (1000.0/1270.0) + return current + def Code2FwdRevWatts(self, fwd, rev): # Convert the HermesLite fwd/rev power code to watts forward and reverse + #print (self.hardware.hermes_rev_power, self.hardware.hermes_fwd_power) + fwd = self.hardware.InterpolatePower(fwd) + rev = self.hardware.InterpolatePower(rev) + # Which voltage is forward and reverse depends on the polarity of the current sense transformer + if fwd >= rev: + return fwd, rev + else: + return rev, fwd + def UpdateText(self): + # Temperature + temp = self.Code2Temp() + temp = (" Temp %3.0f" % temp) + u'\u2103' + self.text_temperature.SetLabel(temp) + # power amp current + current = self.Code2Current() + current = " PA %4.0f ma" % (1000*current) + self.text_pa_current.SetLabel(current) + # forward and reverse peak power + fwd, rev = self.Code2FwdRevWatts(self.hardware.hermes_fwd_peak, self.hardware.hermes_rev_peak) + # forward less reverse power + power = fwd - rev + if power < 0.0: + power = 0.0 + text = " PEP %3.1f watts" % power + self.text_fwd_power.SetLabel(text) + # SWR based on average power + fwd, rev = self.Code2FwdRevWatts(self.hardware.hermes_fwd_power, self.hardware.hermes_rev_power) + if fwd >= 0.05: + gamma = math.sqrt(rev / fwd) + if gamma < 0.98: + swr = (1.0 + gamma) / (1.0 - gamma) + else: + swr = 99.0 + if swr < 9.95: + text = " SWR %4.2f" % swr + else: + text = " SWR %4.0f" % swr + else: + text = " SWR ---" + self.text_swr.SetLabel(text) + if self.tuner_time > 0: + ret = self.hardware.io_board.Receive(self.REG_ANTENNA_TUNER) + if ret: + code = ret[0] + if code == 0: + t = "ATU OK" + self.tuner_time = 0 + elif code == 0xEE: + t = "ATU RF" + elif code >= 0xF0: + t = "Err 0x%X" % code + self.tuner_time = 0 + else: + t = "ATU 0x%X" % code + self.atu_ctrl.SetText(t) + if code == 0xEE: + if not self.application.spotButton.GetValue(): + self.application.spotButton.SetValue(True, True) + elif self.application.spotButton.GetValue(): + self.application.spotButton.SetValue(False, True) + else: + code = -1 + if code != 0 and self.tuner_time > 0 and time.time() - self.tuner_time > 20: + self.tuner_time = 0 + self.atu_ctrl.SetText("ATU Err") + def OnTextDataMenu(self, event): + btn = event.GetEventObject() + btn.PopupMenu(self.text_data_menu, (0,0)) + def OnDataTemperature(self, event): + self.data_sizer.Replace(self.text_data, self.text_temperature) + self.text_data.Hide() + self.text_data = self.text_temperature + self.text_data.Show() + self.data_sizer.Layout() + def OnDataPaCurrent(self, event): + self.data_sizer.Replace(self.text_data, self.text_pa_current) + self.text_data.Hide() + self.text_data = self.text_pa_current + self.text_data.Show() + self.data_sizer.Layout() + def OnDataFwdPower(self, event): + self.data_sizer.Replace(self.text_data, self.text_fwd_power) + self.text_data.Hide() + self.text_data = self.text_fwd_power + self.text_data.Show() + self.data_sizer.Layout() + def OnDataSwr(self, event): + self.data_sizer.Replace(self.text_data, self.text_swr) + self.text_data.Hide() + self.text_data = self.text_swr + self.text_data.Show() + self.data_sizer.Layout() diff --git a/hiqsdr/__init__.py b/hiqsdr/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/hiqsdr/__init__.py @@ -0,0 +1 @@ +# diff --git a/hiqsdr/quisk_conf.py b/hiqsdr/quisk_conf.py new file mode 100644 index 0000000..0ab51e4 --- /dev/null +++ b/hiqsdr/quisk_conf.py @@ -0,0 +1,33 @@ +# This is a sample config file for the N2ADR 2010 transceiver hardware and for the +# improved version HiQSDR. If you use the HiQSDR you should upgrade your firmware +# to version 1.1. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from hiqsdr import quisk_hardware + +add_imd_button = 1 +add_fdx_button = 1 +latency_millisecs = 50 +# tx_level = {None:120, '60':52} # Use this to change your transmit level. + +#use_rx_udp = 1 # Use this for the N2ADR 2010 hardware +use_rx_udp = 2 # Use this for the HiQSDR +rx_udp_ip = "192.168.2.196" # Sample source IP address +rx_udp_port = 0xBC77 # Sample source UDP port +rx_udp_clock = 122880000 # ADC sample rate in Hertz +rx_udp_decimation = 8 * 8 * 8 # Decimation from clock to UDP sample rate +sample_rate = int(float(rx_udp_clock) / rx_udp_decimation + 0.5) # Don't change this +name_of_sound_capt = "" # We do not capture from the soundcard +name_of_sound_play = "hw:0" +data_poll_usec = 10000 +playback_rate = 48000 + +microphone_name = "hw:1" +tx_ip = rx_udp_ip +key_method = "" # Use internal method +tx_audio_port = 0xBC79 +mic_out_volume = 1.0 + diff --git a/hiqsdr/quisk_hardware.py b/hiqsdr/quisk_hardware.py new file mode 100644 index 0000000..2e18590 --- /dev/null +++ b/hiqsdr/quisk_hardware.py @@ -0,0 +1,426 @@ +# This is a sample hardware file for UDP control. Use this file for my 2010 transceiver +# described in QEX and for the improved version HiQSDR. To turn on the extended +# features in HiQSDR, update your FPGA firmware to version 1.1 or later and use use_rx_udp = 2. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import struct, socket, math, traceback +import _quisk as QS + +from quisk_hardware_model import Hardware as BaseHardware + +DEBUG = 0 + +class Hardware(BaseHardware): + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.got_udp_status = '' # status from UDP receiver + # want_udp_status is a 14-byte string with numbers in little-endian order: + # [0:2] 'St' + # [2:6] Rx tune phase + # [6:10] Tx tune phase + # [10] Tx output level 0 to 255 + # [11] Tx control bits: + # 0x01 Enable CW transmit + # 0x02 Enable all other transmit + # 0x04 Use the HiQSDR extended IO pins not present in the 2010 QEX ver 1.0 + # 0x08 The key is down (software key) + # bits 5 and 4: Transmit sample rate + # 0b00 48k + # 0b01 192k + # 0b10 480k + # 0b11 8k + # 0x40 odyssey: Spot button is in use + # 0x80 odyssey: Mic Boost 20dB + # [12] Rx control bits + # bits 5 through 0 + # Second stage decimation less one, 1-39, six bits + # bits 7, 6 + # 0b00 Prescaler 8, 3-byte samples I and Q; 1440 / 6 = 240 samples per UDP packet + # 0b01 Prescaler 2, 2-byte samples + # 0b10 Prescaler 40, 3-byte samples + # 0b11 Prescaler 2, 1-byte samples + # [13] zero or firmware version number + # The above is used for firmware version 1.0. + # Version 1.1 adds eight more bytes for the HiQSDR conntrol ports: + # [14] X1 connector: Preselect pins 69, 68, 65, 64; Preamp pin 63, Tx LED pin 57 + # [15] Attenuator pins 84, 83, 82, 81, 80 + # [16] More bits: AntSwitch pin 41 is 0x01 + # [17:22] The remaining five bytes are sent as zero. + # Version 1.2 uses the same format as 1.1, but adds the "Qs" command (see below). + # Version 1.3 adds features needed by the new quisk_vna.py program: + # [17] The sidetone volume 0 to 255 + # [18:20] This is vna_count, the number of VNA data points; or zero for normal operation + # [20] The CW delay as specified in the config file + # [21] Control bits: + # 0x01 Switch on tx mirror on rx for adaptive predistortion + # [22:24] Noise blanker level + +# The "Qs" command is a two-byte UDP packet sent to the control port. It returns the hardware status +# as the above string, except that the string starts with "Qs" instead of "St". Do not send the "Qs" command +# from Quisk, as it interferes with the "St" command. The "Qs" command is meant to be used from an +# external program, such as HamLib or a logging program. + +# When vna_count != 0, we are in VNA mode. The start frequency is rx_phase, and for each point tx_phase is added +# to advance the frequency. A zero sample is added to mark the blocks. The samples are I and Q averaged at DC. + + self.rx_phase = 0 + self.tx_phase = 0 + self.tx_level = 0 + self.tx_control = 0 + self.rx_control = 0 + QS.set_sample_bytes(3) + self.vna_count = 0 # VNA scan count; MUST be zero for non-VNA operation + self.cw_delay = conf.cw_delay + self.index = 0 + self.mode = None + self.usingSpot = False + self.band = None + self.rf_gain = 0 + self.sidetone_volume = 0 # sidetone volume 0 to 255 + self.repeater_freq = None # original repeater output frequency + self.HiQSDR_Connector_X1 = 0 + self.HiQSDR_Attenuator = 0 + self.HiQSDR_Bits = 0 + try: + if conf.radio_sound_mic_boost: + self.tx_control = 0x80 + except: + pass + if conf.use_rx_udp == 2: # Set to 2 for the HiQSDR + self.rf_gain_labels = ('RF 0 dB', 'RF +10', 'RF -10', 'RF -20', 'RF -30') + self.antenna_labels = ('Ant 1', 'Ant 2') + self.firmware_version = None # firmware version is initially unknown + self.rx_udp_socket = None + self.vfo_frequency = 0 # current vfo frequency + self.tx_frequency = 0 + self.decimations = [] # supported decimation rates + for dec in (40, 20, 10, 8, 5, 4, 2): + self.decimations.append(dec * 64) + self.decimations.append(80) + self.decimations.append(64) + if self.conf.fft_size_multiplier == 0: + self.conf.fft_size_multiplier = 6 # Set size needed by VarDecim + def open(self): + # Create the proper broadcast address for rx_udp_ip. + nm = self.conf.rx_udp_ip_netmask.split('.') + ip = self.conf.rx_udp_ip.split('.') + nm = list(map(int, nm)) + ip = list(map(int, ip)) + bc = '' + for i in range(4): + x = (ip[i] | ~ nm[i]) & 0xFF + bc = bc + str(x) + '.' + self.broadcast_addr = bc[:-1] + # This socket is used for the Simple Network Discovery Protocol by AE4JY + self.socket_sndp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket_sndp.setblocking(0) + self.socket_sndp.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + self.sndp_request = bytearray(56) + self.sndp_request[0] = 56 + self.sndp_request[1] = 0 + self.sndp_request[2] = 0x5A + self.sndp_request[3] = 0xA5 + self.sndp_active = self.conf.sndp_active + # conf.rx_udp_port is used for returning ADC samples + # conf.rx_udp_port + 1 is used for control + self.rx_udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.rx_udp_socket.setblocking(0) + self.rx_udp_socket.connect((self.conf.rx_udp_ip, self.conf.rx_udp_port + 1)) + return QS.open_rx_udp(self.conf.rx_udp_ip, self.conf.rx_udp_port) + def close(self): + if self.rx_udp_socket: + self.rx_udp_socket.close() + self.rx_udp_socket = None + def ReturnFrequency(self): # Return the current tuning and VFO frequency + return None, None # frequencies have not changed + def ReturnVfoFloat(self, freq=None): # Return the accurate VFO as a float + if freq is None: + rx_phase = self.rx_phase + else: + rx_phase = int(float(freq) / self.conf.rx_udp_clock * 2.0**32 + 0.5) & 0xFFFFFFFF + return float(rx_phase) * self.conf.rx_udp_clock / 2.0**32 + def ChangeFrequency(self, tx_freq, vfo_freq, source='', band='', event=None): + if vfo_freq != self.vfo_frequency: + self.vfo_frequency = vfo_freq + self.rx_phase = int(float(vfo_freq - self.transverter_offset) / self.conf.rx_udp_clock * 2.0**32 + 0.5) & 0xFFFFFFFF + if tx_freq and tx_freq > 0: + self.tx_frequency = tx_freq + self.tx_phase = int(float(tx_freq - self.transverter_offset) / self.conf.rx_udp_clock * 2.0**32 + 0.5) & 0xFFFFFFFF + self.NewUdpStatus() + return tx_freq, vfo_freq + def RepeaterOffset(self, offset=None): # Change frequency for repeater offset during Tx + if offset is None: # Return True if frequency change is complete + self.HeartBeat() + return self.want_udp_status == self.got_udp_status + if offset == 0: # Change back to the original frequency + if self.repeater_freq is None: # Frequency was already reset + return self.want_udp_status == self.got_udp_status + self.tx_frequency = self.repeater_freq + self.repeater_freq = None + else: # Shift to repeater input frequency + self.repeater_freq = self.tx_frequency + offset = int(offset * 1000) # Convert kHz to Hz + self.tx_frequency += offset + self.tx_phase = int(float(self.tx_frequency - self.transverter_offset) / self.conf.rx_udp_clock * 2.0**32 + 0.5) & 0xFFFFFFFF + self.NewUdpStatus(True) + return False + def ChangeMode(self, mode): + # mode is a string: "USB", "AM", etc. + self.mode = mode + self.tx_control &= ~0x03 # Erase last two bits + if self.vna_count: + pass + elif self.usingSpot: + self.tx_control |= 0x02 + elif mode in ("CWL", "CWU"): + self.tx_control |= 0x01 + else: + self.tx_control |= 0x02 + self.SetTxLevel() + def ChangeBand(self, band): + # band is a string: "60", "40", "WWV", etc. + BaseHardware.ChangeBand(self, band) + self.band = band + self.HiQSDR_Connector_X1 &= ~0x0F # Mask in the last four bits + self.HiQSDR_Connector_X1 |= self.conf.HiQSDR_BandDict.get(band, 0) & 0x0F + self.SetTxLevel() + def SetTxLevel(self): + # As tx_level varies from 50 to 200, the output level changes from 263 to 752 mV + # So 0 to 255 is 100 to 931, or 1.0 to 9.31; v = 1.0 + 0.0326 * level + if not self.vna_count: + try: + self.tx_level = self.conf.tx_level[self.band] + except KeyError: + self.tx_level = self.conf.tx_level.get(None, 127) # The default + if self.mode[0:3] in ('DGT', 'FDV'): # Digital modes; change power by a percentage + reduc = self.application.digital_tx_level + else: + reduc = self.application.tx_level + level = 1.0 + self.tx_level * 0.0326 + level *= math.sqrt(reduc / 100.0) # Convert from a power to an amplitude + self.tx_level = int((level - 1.0) / 0.0326 + 0.5) + if self.tx_level < 0: + self.tx_level = 0 + elif self.tx_level > 255: + self.tx_level = 255 + self.NewUdpStatus() + def OnButtonRfGain(self, event): + # The HiQSDR attenuator is five bits: 2, 4, 8, 10, 20 dB + btn = event.GetEventObject() + n = btn.index + self.HiQSDR_Connector_X1 &= ~0x10 # Mask in the preamp bit + if n == 0: # 0dB + self.HiQSDR_Attenuator = 0 + self.rf_gain = 0 + elif n == 1: # +10 + self.HiQSDR_Attenuator = 0 + self.HiQSDR_Connector_X1 |= 0x10 + self.rf_gain = 10 + elif n == 2: # -10 + self.HiQSDR_Attenuator = 0x08 + self.rf_gain = -10 + elif n == 3: # -20 + self.HiQSDR_Attenuator = 0x10 + self.rf_gain = -20 + elif n == 4: # -30 + self.HiQSDR_Attenuator = 0x18 + self.rf_gain = -30 + else: + self.HiQSDR_Attenuator = 0 + self.rf_gain = 0 + print ('Unknown RfGain') + self.NewUdpStatus() + def OnButtonPTT(self, event): + # This feature requires firmware version 1.1 or higher + if self.firmware_version: + btn = event.GetEventObject() + if btn.GetValue(): # Turn the software key bit on or off + self.tx_control |= 0x08 + else: + self.tx_control &= ~0x08 + self.NewUdpStatus(True) # Prompt update for PTT + def OnButtonAntenna(self, event): + # This feature requires extended IO + btn = event.GetEventObject() + if btn.index: + self.HiQSDR_Bits |= 0x01 + else: + self.HiQSDR_Bits &= ~0x01 + self.NewUdpStatus() + def ChangeSidetone(self, value): # The sidetone volume changed + self.sidetone_volume = int(value * 255.1) # Change 0.0-1.0 to 0-255 + self.NewUdpStatus() + def HeartBeat(self): + if self.sndp_active: # AE4JY Simple Network Discovery Protocol - attempt to set the FPGA IP address + try: + if DEBUG: print("Sndp send") + self.socket_sndp.sendto(self.sndp_request, (self.broadcast_addr, 48321)) + data, ffrom = self.socket_sndp.recvfrom(1024) + if DEBUG: print("Sndp From", ffrom, "Data", repr(data)) + except: + # traceback.print_exc() + pass + else: + data = bytearray(data) + if len(data) == 56 and data[5:14] == bytearray(b'HiQSDR-v1'): + ip = self.conf.rx_udp_ip.split('.') + t = data[0:4] + t.append(2) + t += data[5:37] + t.append(int(ip[3])) + t.append(int(ip[2])) + t.append(int(ip[1])) + t.append(int(ip[0])) + t += bytearray(12) + t.append(self.conf.rx_udp_port & 0xFF) + t.append(self.conf.rx_udp_port >> 8) + t.append(0) + if DEBUG: print("Sndp reply", repr(t)) + self.socket_sndp.sendto(t, (self.broadcast_addr, 48321)) + try: # receive the old status if any + data = self.rx_udp_socket.recv(1024) + if DEBUG: + self.PrintStatus(' got ', data) + except: + pass + else: + data = bytearray(data) + if data[0:2] == b'St': + self.got_udp_status = data + if self.firmware_version is None: # get the firmware version + if self.want_udp_status[0:13] != self.got_udp_status[0:13]: + try: + self.rx_udp_socket.send(self.want_udp_status) + if DEBUG: + self.PrintStatus('Start', self.want_udp_status) + except: + pass + else: # We got a correct response. + self.firmware_version = self.got_udp_status[13] # Firmware version is returned here + if DEBUG: + print ('Got version', self.firmware_version) + if self.firmware_version > 0 and self.conf.use_rx_udp == 2: + self.tx_control |= 0x04 # Use extra control bytes + self.sndp_active = False + self.NewUdpStatus() + else: + if self.want_udp_status != self.got_udp_status: + if DEBUG: + self.PrintStatus('Have ', self.got_udp_status) + self.PrintStatus(' send', self.want_udp_status) + try: + self.rx_udp_socket.send(self.want_udp_status) + except: + pass + elif DEBUG: + self.rx_udp_socket.send(b'Qs') + def PrintStatus(self, msg, data): + print (msg, ' ', end=' ') + print (data[0:2], end=' ') + for c in data[2:]: + print ("%2X" % c, end=' ') + print () + def GetFirmwareVersion(self): + return self.firmware_version + def OnSpot(self, level): + # level is -1 for Spot button Off; else the Spot level 0 to 1000. + # The Spot button sets the mode to SSB-equivalent for CW so that the Spot level works. + if level >= 0 and not self.usingSpot: # Spot was turned on + self.usingSpot = True + self.tx_control |= 0x40 + self.ChangeMode(self.mode) + elif level < 0 and self.usingSpot: # Spot was turned off + self.usingSpot = False + self.tx_control &= ~0x40 + self.ChangeMode(self.mode) + def OnBtnFDX(self, is_fdx): # Status of FDX button, 0 or 1 + if is_fdx: + self.HiQSDR_Connector_X1 |= 0x20 # Mask in the FDX bit + else: + self.HiQSDR_Connector_X1 &= ~0x20 + self.NewUdpStatus() + def VarDecimGetChoices(self): # return text labels for the control + clock = self.conf.rx_udp_clock + l = [] # a list of sample rates + for dec in self.decimations: + l.append(str(int(float(clock) / dec / 1e3 + 0.5))) + return l + def VarDecimGetLabel(self): # return a text label for the control + return "Sample rate ksps" + def VarDecimGetIndex(self): # return the current index + return self.index + def VarDecimSet(self, index=None): # set decimation, return sample rate + if index is None: # initial call to set decimation before the call to open() + rate = self.application.vardecim_set # May be None or from different hardware + try: + dec = int(float(self.conf.rx_udp_clock) / rate + 0.5) + self.index = self.decimations.index(dec) + except: + try: + self.index = self.decimations.index(self.conf.rx_udp_decimation) + except: + self.index = 0 + else: + self.index = index + dec = self.decimations[self.index] + if dec >= 128: + self.rx_control = dec // 64 - 1 # Second stage decimation less one + QS.set_sample_bytes(3) + else: + self.rx_control = dec // 16 - 1 # Second stage decimation less one + self.rx_control |= 0b01000000 # Change prescaler to 2 (instead of 8) + QS.set_sample_bytes(2) + self.NewUdpStatus() + return int(float(self.conf.rx_udp_clock) / dec + 0.5) + def VarDecimRange(self): + return (48000, 960000) + def NewUdpStatus(self, do_tx=False): + s = bytearray(b'St') + s = s + struct.pack("read_error++; +*/ + +#include + +void ** Quisk_API; // array of pointers to functions and variables from module _quisk +struct sound_conf * pt_quisk_sound_state; // pointer to quisk_sound_state + +#if ( (PY_VERSION_HEX < 0x02070000) || ((PY_VERSION_HEX >= 0x03000000) && (PY_VERSION_HEX < 0x03010000)) ) +// Old Python interface using CObject +int import_quisk_api(void) +{ + PyObject *c_api_object; + PyObject *module; + + module = PyImport_ImportModule("_quisk"); + if (module == NULL) { + printf("Failure 1 to import Quisk_API\n"); + return -1; + } + c_api_object = PyObject_GetAttrString(module, "QUISK_C_API"); + if (c_api_object == NULL) { + Py_DECREF(module); + printf("Failure 2 to import Quisk_API\n"); + return -1; + } + if (PyCObject_Check(c_api_object)) { + Quisk_API = (void **)PyCObject_AsVoidPtr(c_api_object); + } + else { + printf("Failure 3 to import Quisk_API\n"); + Py_DECREF(c_api_object); + Py_DECREF(module); + return -1; + } + Py_DECREF(c_api_object); + Py_DECREF(module); + pt_quisk_sound_state = (struct sound_conf *)Quisk_API[0]; + return 0; +} +#else +// New Python interface using Capsule +int import_quisk_api(void) +{ + Quisk_API = (void **)PyCapsule_Import("_quisk.QUISK_C_API", 0); + if (Quisk_API == NULL) { + printf("Failure to import Quisk_API\n"); + return -1; + } + pt_quisk_sound_state = (struct sound_conf *)Quisk_API[0]; + return 0; +} +#endif diff --git a/is_key_down.c b/is_key_down.c new file mode 100644 index 0000000..76eddd9 --- /dev/null +++ b/is_key_down.c @@ -0,0 +1,240 @@ +#include // used by quisk.h +#include // Used by quisk.h +#include "quisk.h" + +// This module provides methods to access the state of the key. +// First call quisk_open_key(name) to choose a method and initialize. +// Subsequent key access uses the method chosen. + +static int startup_error = -1; // -1: port not opened; 0: no error opening port; 1: error opening port +static int bit_cts, bit_dsr; // modem bits +static char use_cts, use_dsr; // use of CTS and DSR bits +static int reverse_cts, reverse_dsr; // opposite polarity for cts/dsr +static char port_name[QUISK_SC_SIZE]; // serial port name +static PyObject * start_up(void); // open the serial port +static void shut_down(void); // close the serial port +static void modem_status(void); // test modem bits +#define MSG_SIZE (50 + QUISK_SC_SIZE) + +//int quisk_serial_key_errors = 0; +int quisk_serial_key_down; // The cts or dsr bit for CW key is asserted +int quisk_use_serial_port; // either cts or dsr is being used +int quisk_serial_ptt; // The cts or dsr bit for PTT is asserted + +PyObject * quisk_open_key(PyObject * self, PyObject * args, PyObject * keywds) +{ // return a message for error, or "" for no error + static char * kwlist[] = {"port", "cts", "dsr", NULL} ; + PyObject * msg = NULL; + char * port = NULL; + char * cts = NULL; + char * dsr = NULL; + + quisk_serial_key_down = 0; + quisk_serial_ptt = 0; + if (!PyArg_ParseTupleAndKeywords (args, keywds, "|sss", kwlist, &port, &cts, &dsr)) + return NULL; + //quisk_serial_cts and dsr are "None", "CW", "PTT"; and "when high" or "when low" + if (cts) { + use_cts = * cts; // 'N', 'C', 'P' + reverse_cts = strstr(cts, "when low") != NULL; + } + if (dsr) { + use_dsr = * dsr; // 'N', 'C', 'P' + reverse_dsr = strstr(dsr, "when low") != NULL; + } + if (port) { + if (startup_error == 0) // port is open + shut_down(); + strncpy(port_name, port, QUISK_SC_SIZE - 1); + port_name[QUISK_SC_SIZE - 1] = 0; + if (port_name[0]) + msg = start_up(); + } + if (startup_error == 0 && (use_cts != 'N' || use_dsr != 'N')) + quisk_use_serial_port = 1; + else + quisk_use_serial_port = 0; + if (msg == NULL) + msg = PyUnicode_FromString(""); + return msg; +} + +PyObject * quisk_close_key(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + shut_down(); + Py_INCREF (Py_None); + return Py_None; +} + +void quisk_poll_hardware_key(void) +{ // call frequently to check the modem bits + if ( ! quisk_use_serial_port) + return; + modem_status(); + if (use_cts == 'C') + quisk_serial_key_down = reverse_cts ? bit_cts == 0 : bit_cts != 0; + else if (use_cts == 'P') + quisk_serial_ptt = reverse_cts ? bit_cts == 0 : bit_cts != 0; + if (use_dsr == 'C') + quisk_serial_key_down = reverse_dsr ? bit_dsr == 0 : bit_dsr != 0; + else if (use_dsr == 'P') + quisk_serial_ptt = reverse_dsr ? bit_dsr == 0 : bit_dsr != 0; +} + +#if defined(MS_WINDOWS) +#include +#include +#include +//#include +//#include +#include + +static HANDLE hComm = INVALID_HANDLE_VALUE; // Windows handle to read the serial port + +static void modem_status(void) +{ + DWORD dwModemStatus; + + if (hComm == INVALID_HANDLE_VALUE) { + bit_cts = bit_dsr = 0; + } + else { + if (!GetCommModemStatus(hComm, &dwModemStatus)) { + bit_cts = bit_dsr = 0; // Error in GetCommModemStatus; + } + else { + bit_cts = MS_CTS_ON & dwModemStatus; + bit_dsr = MS_DSR_ON & dwModemStatus; + } + } +} + +static PyObject * start_up(void) +{ + char msg[MSG_SIZE]; + + hComm = CreateFile(port_name, GENERIC_READ, 0, 0, OPEN_EXISTING, + 0, 0); + //FILE_FLAG_OVERLAPPED, 0); + if (hComm == INVALID_HANDLE_VALUE) { + snprintf(msg, MSG_SIZE, "Open Morse key serial port %s failed.", port_name); + startup_error = 1; + return PyUnicode_FromString(msg); + } + startup_error = 0; + return PyUnicode_FromString(""); +} + +static void shut_down(void) +{ + if (hComm != INVALID_HANDLE_VALUE) + CloseHandle(hComm); + hComm = INVALID_HANDLE_VALUE; + startup_error = -1; + quisk_serial_key_down = 0; + quisk_use_serial_port = 0; + quisk_serial_ptt = 0; +} + +// Changes for MacOS support thanks to Mario, DL3LSM. +// Broken by N2ADR April, 2020. +#elif defined(__MACH__) + +static PyObject * start_up(void) +{ + startup_error = 0; + return PyUnicode_FromString(""); +} + +static void shut_down(void) +{ + startup_error = -1; + quisk_serial_key_down = 0; + quisk_use_serial_port = 0; + quisk_serial_ptt = 0; +} + +static void modem_status(void) +{ +} + +#else +// Not MS Windows and not __MACH__: + +// Access the serial port. This code sets DTR high, and monitors DSR and CTS. +// When DSR is high set the RTS signal high. When DSR goes low set RTS low after a delay. + +#include +#include +#include + +static int fdComm = -1; // File descriptor to read the serial port + +static void modem_status(void) +{ + int bits; + struct timeval tv; + double time; + static double time0=0; // time when the key was last down + + if (fdComm >= 0) { + ioctl(fdComm, TIOCMGET, &bits); // read modem bits + bit_cts = bits & TIOCM_CTS; + bit_dsr = bits & TIOCM_DSR; + if (bit_dsr) { + if ( ! (bits & TIOCM_RTS)) { // set RTS + bits |= TIOCM_RTS; + ioctl(fdComm, TIOCMSET, &bits); + } + gettimeofday(&tv, NULL); + time0 = tv.tv_sec + tv.tv_usec / 1.0E6; // time is in seconds + } + else if (bits & TIOCM_RTS) { // clear RTS after a delay + gettimeofday(&tv, NULL); + time = tv.tv_sec + tv.tv_usec / 1.0E6; + if (time - time0 > pt_quisk_sound_state->quiskKeyupDelay * 1E-3) { + bits &= ~TIOCM_RTS; + ioctl(fdComm, TIOCMSET, &bits); + } + } + } +} + +static PyObject * start_up(void) +{ + int bits; + char msg[MSG_SIZE]; + struct timespec tspec; + + fdComm = open(port_name, O_RDWR | O_NOCTTY); + if (fdComm < 0) { + snprintf(msg, MSG_SIZE, "Open morse key serial port %s failed.", port_name); + startup_error = 1; + return PyUnicode_FromString(msg); + } + else { + ioctl(fdComm, TIOCMGET, &bits); // read modem bits + bits |= TIOCM_DTR; // Set DTR + bits &= ~TIOCM_RTS; // Clear RTS at first + ioctl(fdComm, TIOCMSET, &bits); + } + tspec.tv_sec = 0; + tspec.tv_nsec = 10000 * 1000; + nanosleep(&tspec, NULL); + startup_error = 0; + return PyUnicode_FromString(""); +} + +static void shut_down(void) +{ + if (fdComm >= 0) + close(fdComm); + fdComm = -1; + startup_error = -1; + quisk_serial_key_down = 0; + quisk_use_serial_port = 0; + quisk_serial_ptt = 0; +} +#endif diff --git a/libfftw3-3.dll b/libfftw3-3.dll new file mode 100644 index 0000000..f5a97b4 Binary files /dev/null and b/libfftw3-3.dll differ diff --git a/libgcc_s_dw2-1.dll b/libgcc_s_dw2-1.dll new file mode 100644 index 0000000..8a15591 Binary files /dev/null and b/libgcc_s_dw2-1.dll differ diff --git a/libusb.txt b/libusb.txt new file mode 100644 index 0000000..10591b9 --- /dev/null +++ b/libusb.txt @@ -0,0 +1,30 @@ +Notes on libusb and pyusb +========================= + +Libusb provides access to the USB bus from user space. It uses the +files in /dev/bus/usb/*/*. The following commands are useful on Linux: + +List devices on the USB bus: + lsusb +Add -d 16c0:05dc for a specific device, -v for more information. + +List file permissions for bus 001 device 005: + ls -l /dev/bus/usb/001/005 +These permissions default to 660 group root. + +List udev information for bus 001 device 005: + udevadm info --query=all --name=/dev/bus/usb/001/005 --attribute-walk +These items can be used in udev rules. + +Default USB permissions do not allow a non-root user to write to the bus. +To change permissions, add a rule to /etc/udev/rules.d/local.rules like this: + SUBSYSTEM=="usb", ATTR{idVendor}=="16c0" , ATTR{idProduct}=="05dc", MODE="0666", GROUP="dialout" +Then notify udev with "udevadm control --reload_rules", or /etc/init.d/udev/restart. But +on my system, I need to plug in the SoftRock and reboot. + +To install Libusb on Windows, follow the instructions in + http://sourceforge.net/apps/trac/libusb-win32/wiki. +Run the program libusb-win32*/bin/inf-wizard.exe. +On Windows, when libusb-win32 is properly installed, Device Manager reports a +top-level device "libusb-win32 devices" and a sub-device "DG8SAQ-I2C". Otherwise +it reports "Unknown Device" under "Universal Serial Bus controllers". diff --git a/libwdsp.dll b/libwdsp.dll new file mode 100644 index 0000000..15b49a3 Binary files /dev/null and b/libwdsp.dll differ diff --git a/libwinpthread-1.dll b/libwinpthread-1.dll new file mode 100644 index 0000000..5c31e0a Binary files /dev/null and b/libwinpthread-1.dll differ diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..2185a92 --- /dev/null +++ b/license.txt @@ -0,0 +1,129 @@ +This software is Copyright (C) 2007-2024 by James C. Ahlstrom, and is +licensed for use under the GNU General Public License (GPL). +See http://www.opensource.org. +Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!! + +The GNU General Public License (GPL) +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. + + c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + One line to give the program's name and a brief idea of what it does. + Copyright (C) + + 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 of the License, 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; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. + + signature of Ty Coon, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. diff --git a/makefile b/makefile new file mode 100644 index 0000000..bfb9c69 --- /dev/null +++ b/makefile @@ -0,0 +1,18 @@ +.PHONY: quisk + +quisk: + python3 setup.py build_ext --force --inplace + @echo + +quisk3: + python3 setup.py build_ext --force --inplace + @echo + +soapy3: + (cd soapypkg; make soapy3) + +afedrinet3: + (cd afedrinet; make afedrinet3) + +perseus3: + (cd perseuspkg; make perseus3) diff --git a/microphone.c b/microphone.c new file mode 100644 index 0000000..bec0537 --- /dev/null +++ b/microphone.c @@ -0,0 +1,1608 @@ +#include +#include +#include +#include +#include +#include +#include "quisk.h" +#include +#include "microphone.h" +#include "filter.h" +#include "freedv.h" + +#ifdef MS_WINDOWS +#include +static int mic_cleanup = 0; // must clean up winsock +#else +#include +#include +#include +#endif + +#define DEBUG 0 +#define DEBUG_LEVEL 0 + +#if DEBUG_LEVEL || DEBUG_IO || DEBUG +static int debug_timer = 1; // count up number of samples +#endif + +// The microphone samples must be 48000 sps or 8000 sps. The output sample +// rate is always MIC_OUT_RATE samples per second + +// FM needs pre-emphasis and de-emphasis. See vk1od.net/FM/FM.htm for details. +// For IIR design, see http://www.abvolt.com/research/publications2.htm. + +// Microhone preemphasis: boost high frequencies 0.00 to 1.00 +double quisk_mic_preemphasis; +// Microphone clipping; try 3.0 or 4.0 +double quisk_mic_clip; + +// If true, decimate 48000 sps mic samples to 8000 sps for processing +#define DECIM_8000 1 + +struct alc { + complex double * buffer; + int buf_size; + int index; + int block_index; + int counter; + int fault; + double max_magn; + double gain_now[20]; + double gain_max; + double gain_min; + double gain_change; + double next_change; + double final_gain; +} ; + +// These are external: +int mic_max_display; // display value of maximum microphone signal level 0 to 2**15 - 1 +int quiskSpotLevel = -1; // level is -1 for Spot button Off; else the Spot level 0 to 1000. +int quiskImdLevel = 500; // level for rxMode IMD, 0 to 1000 +int hermes_mox_bit; // mox bit to send to Hermes + +static SOCKET mic_socket = INVALID_SOCKET; // send microphone samples to a socket +static double mic_agc_level = 0.10; // Mic levels below this are noise and are ignored + +static int mic_level; // maximum microphone signal level for display +static int mic_timer; // time to display maximum mic level +static int align4; // add two bytes to start of audio samples to align to 4 bytes +static double modulation_index = 1.6; // For FM transmit, the modulation index + +static int is_vox = 0; // Is the VOX level exceeded? +static int vox_level = CLIP16; // VOX trigger level as a number 0 to CLIP16 +static int timeVOX = 2000; // VOX hang time in milliseconds +static int tx_sample_rate = 48000; // Used for SoapySDR +static int reverse_tx_sideband; + +static int doTxCorrect = 0; // Corrections for UDP sample transmit +static double TxCorrectLevel; +static complex TxCorrectDc; + +// Used for the Hermes protocol +#define HERMES_TX_BUF_SAMPLES 4800 // buffer size in I/Q samples (two shorts) +#define HERMES_TX_BUF_SHORTS (HERMES_TX_BUF_SAMPLES * 2) +static int hermes_read_index; // index to read from buffer +static int hermes_write_index; // index to write to buffer +static int hermes_num_samples; // number of samples in the buffer +static short hermes_buf[HERMES_TX_BUF_SHORTS]; // buffer to store Tx I/Q samples waiting to be sent at 48 ksps +static int hermes_filter_rx; // hermes filter to use for Rx +static int hermes_filter_tx; // hermes filter to use for Tx + +static void serial_key_samples(complex double *, int); +static play_state_t last_play_state; + +#define TX_BLOCK_SHORTS 600 // transmit UDP packet with this many shorts (two bytes) (perhaps + 1) +#define MIC_MAX_HOLD_TIME 400 // Time to hold the maximum mic level on the Status screen in milliseconds + +// If USE_GET_SIN is not zero, replace mic samples with a sin wave at a +// frequency determined by the sidetone slider and an amplitude determined +// by the Spot button level. LEVEL FAILS +// If USE_GET_SIN is 1, pass these samples through the transmit filters. +// If USE_GET_SIN is 2, transmit these samples directly. +#define USE_GET_SIN 0 + +// If USE_2TONE is not zero, replace samples with a 2-tone test signal. +#define USE_2TONE 0 + +#if USE_GET_SIN +static void get_sin(complex double * cSamples, int count) +{ // replace mic samples with a sin wave + int i; + double freq; + complex double phase1; // Phase increment + static complex double vector1 = CLIP32 / 2; + + // Use the sidetone slider 0 to 1000 to set frequency + //freq = (quisk_sidetoneCtrl - 500) / 1000.0 * MIC_OUT_RATE; + freq = quisk_sidetoneCtrl * 5; + freq = ((int)freq / 50) * 50; +#if USE_GET_SIN == 2 + phase1 = cexp(I * 2.0 * M_PI * freq / MIC_OUT_RATE); + count *= MIC_OUT_RATE / quisk_sound_state.mic_sample_rate; +#else + phase1 = cexp(I * 2.0 * M_PI * freq / quisk_sound_state.mic_sample_rate); +#endif + for (i = 0; i < count; i++) { + vector1 *= phase1; + cSamples[i] = vector1; + } +#if DEBUG_IO || DEBUG + if (debug_timer == 0) + printf ("get_sin freq %.0lf\n", freq); +#endif +} +#endif + +#if USE_2TONE +static void get_2tone(complex double * cSamples, int count) +{ // replace mic samples + int i; + static complex double phase1=0, phase2; // Phase increment + static complex double vector1; + static complex double vector2; + + if (phase1 == 0) { // initialize + phase1 = cexp((I * 2.0 * M_PI * IMD_TONE_1) / quisk_sound_state.mic_sample_rate); + phase2 = cexp((I * 2.0 * M_PI * IMD_TONE_2) / quisk_sound_state.mic_sample_rate); + vector1 = CLIP32 / 2.0; + vector2 = CLIP32 / 2.0; + } + for (i = 0; i < count; i++) { + vector1 *= phase1; + vector2 *= phase2; + cSamples[i] = (vector1 + vector2); + } +} +#endif + +static double CcmPeak(double * dsamples, complex double * csamples, int count) +{ + int i, j; + complex double csample; + double dtmp, dsample, newlevel, oldlevel; + static double out_short, out_long; + static struct Ccmpr { + int buf_size; + int index_read; + double themax; + double level; + double * d_samp; + complex double * c_samp; + double * levl; + } dat = {0}; + + if ( ! dat.buf_size) { // initialize; the sample rate is 8000 + dat.buf_size = 8000 * 30 / 1000; // total delay in samples + dat.index_read = 0; // index to output; and then write a new sample here + dat.themax = 1.0; // maximum level in the buffer + dat.level = 1.0; // current output level + dat.d_samp = (double *) malloc(dat.buf_size * sizeof(double)); // buffer for double samples + dat.c_samp = (complex double *) malloc(dat.buf_size * sizeof(complex double)); // buffer for complex samples + dat.levl = (double *) malloc(dat.buf_size * sizeof(double)); // magnitude of the samples + for (i = 0; i < dat.buf_size; i++) { + dat.d_samp[i] = 0; + dat.c_samp[i] = 0; + dat.levl[i] = 1.0; + } + dtmp = 1.0 / 8000; // sample time + out_short = 1.0 - exp(- dtmp / 0.010); // short time constant + out_long = 1.0 - exp(- dtmp / 3.000); // long time constant + return 1.0; + } + for (i = 0; i < count; i++) { + if (dsamples) { + dsample = dsamples[i]; + dsamples[i] = dat.d_samp[dat.index_read] / dat.level; // FIFO output + dat.d_samp[dat.index_read] = dsample; // write new sample at read index + newlevel = fabs(dsample); + } + else { + csample = csamples[i]; + csamples[i] = dat.c_samp[dat.index_read] / dat.level; // FIFO output + dat.c_samp[dat.index_read] = csample; // write new sample at read index + newlevel = cabs(csample); + } + oldlevel = dat.levl[dat.index_read]; + dat.levl[dat.index_read] = newlevel; + if (newlevel < dat.themax && oldlevel < dat.themax) { // some other sample is the maximum + // no change to dat.themax + } + else if (newlevel > dat.themax && newlevel > oldlevel) { // newlevel is the maximum + dat.themax = newlevel; + } + else { // search for the maximum level + dat.themax = 0; // Find the maximim level in the buffer + for (j = 0; j < dat.buf_size; j++) { + if (dat.levl[j] > dat.themax) + dat.themax = dat.levl[j]; + } + } +// Increase dat.level if the maximum level is greater than 1.0; +// decrease it slowly back to 1.0 if it is lower. Output is modulated by dat.level. + if (dat.themax > 1.0) // increase rapidly to the peak level + dat.level = dat.level * (1.0 - out_short) + dat.themax * out_short; + else // decrease slowly back to 1.0 + dat.level = dat.level * (1.0 - out_long) + 1.0 * out_long; + if (++dat.index_read >= dat.buf_size) + dat.index_read = 0; + } + return dat.level; +} + +static void init_alc(struct alc * pt, int size) +{ // Call first to set the buffer size. Then call to initialize the structure on each key down. + int i; + + if (pt->buffer == NULL) { + pt->buf_size = size; // do not change the size + pt->buffer = (complex double *)malloc(size * sizeof(complex double)); + for (i = 0; i < 20; i++) // initial gain by rx_mode + switch(i) { + case DGT_U: + case DGT_L: + case DGT_IQ: + pt->gain_now[i] = 1.4; + break; + case FDV_U: + case FDV_L: + pt->gain_now[i] = 2.0; + break; + default: + pt->gain_now[i] = 1.0; + break; + } + } + pt->index = 0; + pt->block_index = 0; + pt->counter = 0; + pt->fault = 0; + pt->max_magn = 0; + pt->gain_max = 3.0; + pt->gain_min = 0.1; + pt->gain_change = 0; + pt->next_change = 0; + pt->final_gain = 0; + for (i = 0; i < pt->buf_size; i++) + pt->buffer[i] = 0; +} + +static void process_alc(complex double * cSamples, int count, struct alc * pt, rx_mode_type rx_mode) +{ // Automatic Level Control (ALC) + int i; + double d, magn; + complex double csamp; + + for (i = 0; i < count; i++) { + csamp = cSamples[i]; // new sample to add to buffer + cSamples[i] = pt->buffer[pt->index] * pt->gain_now[rx_mode]; // remove sample from buffer and apply gain +#if DEBUG_LEVEL || DEBUG_IO || DEBUG + magn = cabs(cSamples[i]); + if (magn >= CLIP16) + printf("ALC clip gain %9.6f level %9.6f\n", pt->gain_now[rx_mode], magn / CLIP16); +#endif + pt->buffer[pt->index] = csamp; // add new sample to buffer + magn = cabs(csamp); // measure new sample + if (magn * (pt->gain_now[rx_mode] + pt->gain_change * pt->buf_size) > (CLIP16 - 10)) { + pt->gain_change = ((CLIP16 - 10) / magn - pt->gain_now[rx_mode]) / pt->buf_size; + pt->final_gain = pt->gain_now[rx_mode] + pt->gain_change * pt->buf_size; + if (pt->final_gain > pt->gain_max) { + pt->final_gain = pt->gain_max; + pt->gain_change = (pt->final_gain - pt->gain_now[rx_mode]) / pt->buf_size; + } + else if (pt->final_gain < pt->gain_min) { + pt->final_gain = pt->gain_min; + pt->gain_change = (pt->final_gain - pt->gain_now[rx_mode]) / pt->buf_size; + } + pt->block_index = pt->index; + pt->counter = 0; + pt->fault = 0; + pt->next_change = 1E10; + //printf("ALC DEC: gain %9.6f change %9.6f final gain %9.6f\n", pt->gain_now[rx_mode], pt->gain_change, pt->final_gain); + } + else if (pt->index == pt->block_index) { + //d = cabs(cSamples[i]) / (CLIP16 - 10); + //printf("ALC Fin: gain %9.6f out level %9.6f\n", pt->gain_now[rx_mode], d); +#if 0 + alc_start_gain = alc_gain; + k = alc_block_index; + for (j = 1; j < pt->buf_size; j++) { + if (++k >= pt->buf_size) + k = 0; + magn = cabs(alc_buffer[k]); + if (magn < 100) { + alc_fault++; + continue; + } + else { + d = ((CLIP16 - 10) / magn - alc_start_gain) / j; + if ( alc_next_change > d) + alc_next_change = d; + } + } +#endif + d = 5.0; // number of seconds to double gain + d = 1.0 / (48000.0 * d); + if (pt->next_change > d) + pt->next_change = d; + if (pt->next_change != 1E10 && pt->fault < pt->buf_size - 10) { + pt->gain_change = pt->next_change; + } + pt->final_gain = pt->gain_now[rx_mode] + pt->gain_change * pt->buf_size; + if (pt->final_gain > pt->gain_max) { + pt->final_gain = pt->gain_max; + pt->gain_change = (pt->final_gain - pt->gain_now[rx_mode]) / pt->buf_size; + } + else if (pt->final_gain < pt->gain_min) { + pt->final_gain = pt->gain_min; + pt->gain_change = (pt->final_gain - pt->gain_now[rx_mode]) / pt->buf_size; + } + //printf("ALC New: gain %9.6f change %9.6f final gain %9.6f\n", pt->gain_now[rx_mode], pt->gain_change, pt->final_gain); + pt->fault = 0; + pt->counter = 0; + pt->next_change = 1E10; + } + else { + if (magn < 100) { + pt->fault++; + } + else { + d = ((CLIP16 - 10) / magn - pt->final_gain) / ++pt->counter; + if ( pt->next_change > d) + pt->next_change = d; + } + } + pt->gain_now[rx_mode] += pt->gain_change; + if (++pt->index >= pt->buf_size) + pt->index = 0; + } +#if DEBUG_LEVEL || DEBUG_IO || DEBUG + for (i = 0; i < count; i++) { + magn = cabs(cSamples[i]); + if (pt->max_magn < magn) + pt->max_magn = magn; + } + if (debug_timer == 0) { + printf("ALC Out: gain %9.6f max lvl%9.6f final gain %9.6f\n", pt->gain_now[rx_mode], pt->max_magn / CLIP16, pt->final_gain); + pt->max_magn = 0; + } +#endif +} + +static int tx_filter(complex double * filtered, int count) +{ // Input samples are creal(filtered), output is filtered. The input rate must be 8000 or 48000 sps. + int i, is_ssb; + int sample_rate = 8000; + double dsample, dtmp, magn; + complex double csample; + static double inMax=0.3; + static double x_1=0; + static double aaa, bbb, ccc, Xmin, Xmax, Ymax; + static int samples_size = 0; + static double * dsamples = NULL; + static complex double * csamples = NULL; + static double time_long, time_short; + static struct quisk_dFilter filtDecim, dfiltInterp; + static struct quisk_dFilter filtAudio1, filtAudio2, dfiltAudio3; + static struct quisk_cFilter cfiltAudio3, cfiltInterp; + static struct quisk_dFilter filter1={NULL}, filter2; +#if DEBUG_IO || DEBUG + char * clip; + static double dbOut = 0, Level0 = 0, Level1 = 0, Level2 = 0, Level3 = 0, Level4 = 0; +#endif + is_ssb = (rxMode == LSB || rxMode == USB); + if (!filtered) { // initialization + if (! filter1.dCoefs) { + quisk_filt_dInit(&filter1, quiskMicFilt8Coefs, sizeof(quiskMicFilt8Coefs)/sizeof(double)); + quisk_filt_dInit(&filter2, quiskMicFilt8Coefs, sizeof(quiskMicFilt8Coefs)/sizeof(double)); + quisk_filt_dInit(&filtDecim, quiskLpFilt48Coefs, sizeof(quiskLpFilt48Coefs)/sizeof(double)); + quisk_filt_dInit(&dfiltInterp, quiskLpFilt48Coefs, sizeof(quiskLpFilt48Coefs)/sizeof(double)); + quisk_filt_cInit(&cfiltInterp, quiskLpFilt48Coefs, sizeof(quiskLpFilt48Coefs)/sizeof(double)); + quisk_filt_dInit(&filtAudio1, quiskFiltTx8kAudioB, sizeof(quiskFiltTx8kAudioB)/sizeof(double)); + quisk_filt_dInit(&filtAudio2, quiskFiltTx8kAudioB, sizeof(quiskFiltTx8kAudioB)/sizeof(double)); + quisk_filt_dInit(&dfiltAudio3, quiskFiltTx8kAudioB, sizeof(quiskFiltTx8kAudioB)/sizeof(double)); + quisk_filt_cInit(&cfiltAudio3, quiskFiltTx8kAudioB, sizeof(quiskFiltTx8kAudioB)/sizeof(double)); + dtmp = 1.0 / sample_rate; // sample time + time_long = 1.0 - exp(- dtmp / 3.000); + time_short = 1.0 - exp(- dtmp / 0.005); + Ymax = pow(10.0, - 1 / 20.0); // maximum y + Xmax = pow(10.0, 3 / 20.0); // x where slope is zero; for x > Xmax, y == Ymax + Xmin = Ymax - fabs(Ymax - Xmax); // x where slope is 1 and y = x; start of compression + aaa = 1.0 / (2.0 * (Xmin - Xmax)); // quadratic + bbb = -2.0 * aaa * Xmax; + ccc = Ymax - aaa * Xmax * Xmax - bbb * Xmax; +#if DEBUG_IO || DEBUG + printf("Compress to %.2lf dB from %.2lf to %.2lf dB\n", + 20 * log10(Ymax), 20 * log10(Xmin), 20 * log10(Xmax)); +#endif + } + if (is_ssb) { + quisk_filt_tune(&filter1, 1650.0 / sample_rate, rxMode != LSB); + quisk_filt_tune(&filter2, 1650.0 / sample_rate, rxMode != LSB); + } + return 0; + } + // check size of dsamples[] and csamples[] buffer + if (count > samples_size) { + samples_size = count * 2; + if (dsamples) + free(dsamples); + if (csamples) + free(csamples); + dsamples = (double *)malloc(samples_size * sizeof(double)); + csamples = (complex double *)malloc(samples_size * sizeof(complex double)); + } + // copy to dsamples[], normalize to +/- 1.0 + for (i = 0; i < count; i++) + dsamples[i] = creal(filtered[i]) / CLIP16; + // Decimate to 8000 Hz + if (quisk_sound_state.mic_sample_rate != sample_rate) + count = quisk_dDecimate(dsamples, count, &filtDecim, quisk_sound_state.mic_sample_rate / sample_rate); + // restrict bandwidth 300 to 2700 Hz + count = quisk_dFilter(dsamples, count, &filtAudio1); +#if DEBUG_IO || DEBUG + // Measure peak input audio level + for (i = 0; i < count; i++) { + magn = fabs(dsamples[i]); + if (magn > Level0) + Level0 = magn; + } +#endif + // high pass filter for preemphasis: See Radcom, January 2010, page 76. + // quisk_mic_preemphasis == 1 was measured as 6 dB / octave. + // gain at 800 Hz was measured as 0.104672. + for (i = 0; i < count; i++) { + dtmp = dsamples[i]; + dsamples[i] = dtmp - quisk_mic_preemphasis * x_1; + x_1 = dtmp; // delayed sample + dsamples[i] *= 2; // compensate for loss +#if DEBUG_IO || DEBUG + magn = fabs(dsamples[i]); + if (magn > Level1) + Level1 = magn; +#endif + } + if (is_ssb) { // SSB + // FIR bandpass filter; separate into I and Q + for (i = 0; i < count; i++) { + csample = quisk_dC_out(dsamples[i], &filter1) * 2.0; // filter loss 0.5 + // Measure average peak input audio level and normalize + magn = cabs(csample); + if (magn > inMax) + inMax = inMax * (1 - time_short) + time_short * magn; + else if(magn > mic_agc_level) + inMax = inMax * (1 - time_long) + time_long * magn; + else + inMax = inMax * (1 - time_long) + time_long * mic_agc_level; + csample /= inMax; + magn /= inMax; +#if DEBUG_IO || DEBUG + if (magn > Level2) + Level2 = magn; +#endif + // Audio compression. + csample *= quisk_mic_clip; + magn *= quisk_mic_clip; + if (magn > 1.0) + csample = csample / magn; + dsamples[i] = creal(csample); + } + } + else { // AM and FM + // Measure average peak input audio level and normalize + for (i = 0; i < count; i++) { + dsample = dsamples[i]; + magn = fabs(dsample); + if (magn > inMax) + inMax = inMax * (1 - time_short) + time_short * magn; + else if(magn > mic_agc_level) + inMax = inMax * (1 - time_long) + time_long * magn; + else + inMax = inMax * (1 - time_long) + time_long * mic_agc_level; + dsample /= inMax; + magn /= inMax; +#if DEBUG_IO || DEBUG + if (magn > Level2) + Level2 = magn; +#endif + // Audio compression. + dsample *= quisk_mic_clip; + magn *= quisk_mic_clip; + if (magn < Xmin) + dsamples[i] = dsample; + else if (magn > Xmax) + dsamples[i] = copysign(Ymax, dsample); + else + dsamples[i] = copysign(aaa * magn * magn + bbb * magn + ccc, dsample); + } + } + // remove clipping distortion; restrict bandwidth 300 to 2700 Hz + count = quisk_dFilter(dsamples, count, &filtAudio2); + if (is_ssb) { // SSB + // FIR bandpass filter; separate into I and Q + for (i = 0; i < count; i++) { + csamples[i] = quisk_dC_out(dsamples[i], &filter2) * 2.0; // filter loss 0.5 +#if DEBUG_IO || DEBUG + magn = cabs(csamples[i]); + if (magn > Level3) + Level3 = magn; +#endif + } + // round off peaks + CcmPeak(NULL, csamples, count); +#if DEBUG_IO || DEBUG + for (i = 0; i < count; i++) { + magn = cabs(csamples[i]); + if (magn > Level4) + Level4 = magn; + } +#endif + // remove clipping distortion + count = quisk_cDecimate(csamples, count, &cfiltAudio3, 1); + // Interpolate up to 48000 + if (MIC_OUT_RATE != sample_rate) + count = quisk_cInterpolate(csamples, count, &cfiltInterp, MIC_OUT_RATE / sample_rate); + // convert back to 16 bits + for (i = 0; i < count; i++) { + filtered[i] = csamples[i] * CLIP16; +#if DEBUG_IO || DEBUG + magn = cabs(csamples[i]); + if (magn > dbOut) + dbOut = magn; +#endif + } + } + else { // AM and FM +#if DEBUG_IO || DEBUG + for (i = 0; i < count; i++) { + magn = fabs(dsamples[i]); + if (magn > Level3) + Level3 = magn; + } +#endif + // round off peaks + CcmPeak(dsamples, NULL, count); +#if DEBUG_IO || DEBUG + for (i = 0; i < count; i++) { + magn = fabs(dsamples[i]); + if (magn > Level4) + Level4 = magn; + } +#endif + // remove clipping distortion + count = quisk_dFilter(dsamples, count, &dfiltAudio3); + // Interpolate up to 48000 + if (MIC_OUT_RATE != sample_rate) + count = quisk_dInterpolate(dsamples, count, &dfiltInterp, MIC_OUT_RATE / sample_rate); + // convert back to 16 bits + for (i = 0; i < count; i++) { + filtered[i] = dsamples[i] * CLIP16; +#if DEBUG_IO || DEBUG + magn = fabs(dsamples[i]); + if (magn > dbOut) + dbOut = magn; +#endif + } + } +#if DEBUG_IO || DEBUG + if (debug_timer == 0) { + if (dbOut > 1.0) + clip = "Clip"; + else + clip = ""; + dbOut = 20 * log10(dbOut); + printf ("pre %3.1lf dB clip %2.0lf InMax %6.2lf Level0 %6.2lf Level1 %6.2lf Level2 %6.2lf Level3 %6.2lf Level4 %6.2lf dbOut %6.2lf %s\n", + quisk_mic_preemphasis, 20 * log10(quisk_mic_clip), 20 * log10(inMax), 20 * log10(Level0), + 20 * log10(Level1), 20 * log10(Level2), 20 * log10(Level3), 20 * log10(Level4), dbOut, clip); + Level0 = Level1 = Level2 = Level3 = Level4 = dbOut = 0; + } + //QuiskPrintTime(" tx_filter", 2); +#endif + return count; +} + +static int tx_filter_digital(complex double * filtered, int count) +{ // Input samples are creal(filtered), output is filtered. + // This filter has minimal processing and is used for digital modes. + int i; + static int do_init = 1; + + static struct quisk_dFilter filter1; + if (do_init) { // initialization + do_init = 0; + quisk_filt_dInit(&filter1, quiskDgtFilt48Coefs, sizeof(quiskDgtFilt48Coefs)/sizeof(double)); // pass 1350, stop 1650 + } + if ( ! filtered) { // Change to rxMode + quisk_filt_tune(&filter1, 1650.0 / 48000, rxMode != DGT_L && rxMode != LSB); + return 0; + } + for (i = 0; i < count; i++) // FIR bandpass filter; separate into I and Q + filtered[i] = quisk_dC_out(creal(filtered[i]), &filter1) * 2.00; // tuned filter loss 0.5 + //quisk_calc_audio_graph(CLIP16, filtered, NULL, count, 0); + return count; +} + +static int tx_filter_freedv(complex double * filtered, int count, int encode) +{ // Input samples are creal(filtered), output is filtered. + // This filter is used for digital voice. + // changes by Dave Roberts, G8KBB, for additional modes June, 2020. + int i; + // The input sample rate is mic_sample_rate, either 8000 or 48000 sps. + // The FreeDV codec requires an input n_speech_sample_rate of either 8000 or 16000 sps depending on the mode. Input is always real. + // The FreeDV codec will output samples at n_modem_sample_rate of 8000, 16000 or 48000 sps depending on the mode, and output may be real or complex. + double dtmp, magn, dsample; + static int samples_size = 0; + static int last_rx_mode; + static int last_freedv_mode; + static int do_init = 1; + static double aaa, bbb, ccc, Xmin, Xmax, Ymax; + static double time_long, time_short; + static double inMax=0.3; + static double * dsamples = NULL; + static struct quisk_cFilter filter2; + static struct quisk_dFilter filtDecim; + static struct quisk_cFilter cfiltInterp; + + if (do_init) { // initialization + //QuiskWavWriteOpen(&hWav, "jim.wav", 3, 1, 4, 8000, 1.0 / CLIP16); + do_init = 0; + last_rx_mode = -1; + last_freedv_mode = -1; + memset(&filter2, 0, sizeof(filter2)); + quisk_filt_dInit(&filtDecim, quiskLpFilt48Coefs, sizeof(quiskLpFilt48Coefs)/sizeof(double)); // pass 3000, stop 4000 + quisk_filt_cInit(&cfiltInterp, quiskLpFilt48Coefs, sizeof(quiskLpFilt48Coefs)/sizeof(double)); + Ymax = pow(10.0, - 1 / 20.0); // maximum y + Xmax = pow(10.0, 3 / 20.0); // x where slope is zero; for x > Xmax, y == Ymax + Xmin = Ymax - fabs(Ymax - Xmax); // x where slope is 1 and y = x; start of compression + //printf ("Xmin %f\n", Xmin); + aaa = 1.0 / (2.0 * (Xmin - Xmax)); // quadratic + bbb = -2.0 * aaa * Xmax; + ccc = Ymax - aaa * Xmax * Xmax - bbb * Xmax; + } + if (last_freedv_mode != freedv_current_mode) { // correct filter2 depends on freedv mode + last_freedv_mode = freedv_current_mode; + last_rx_mode = -1; // re-tune new filter + if (filter2.cSamples) + free(filter2.cSamples); + filter2.cSamples = NULL; + if (filter2.cpxCoefs) + free(filter2.cpxCoefs); + filter2.cpxCoefs = NULL; + if (filter2.cBuf) + free(filter2.cBuf); + filter2.cBuf = NULL; + switch(freedv_current_mode) { + case FREEDV_MODE_700D: // filter2 is not used + break; + // TODO: add modes that return real samples that must be converted to complex + default: + quisk_filt_cInit(&filter2, quiskFilt53D2Coefs, sizeof(quiskFilt53D2Coefs)/sizeof(double)); // pass 1500, stop 1800 + break; + } + } + if (last_rx_mode != (int)rxMode) { // change to sideband + last_rx_mode = rxMode; + if (rxMode == FDV_U) // upper sideband + quisk_filt_tune((struct quisk_dFilter *)&filter2, 1600.0 / n_modem_sample_rate, 1); + else if (rxMode == FDV_L) // lower sideband + quisk_filt_tune((struct quisk_dFilter *)&filter2, 1600.0 / n_modem_sample_rate, 0); + } + if ( ! filtered) + return 0; + // check size of dsamples[] buffer + if (count > samples_size) { + samples_size = count * 2; + if (dsamples) + free(dsamples); + dsamples = (double *)malloc(samples_size * sizeof(double)); + } + // copy to dsamples[] + for (i = 0; i < count; i++) + dsamples[i] = creal(filtered[i]) / CLIP16; // normalize to 1.0000 + // Decimate to 8000 Hz or to 16000 depending on mode (and hence setting of sample rate) + if (quisk_sound_state.mic_sample_rate == 48000) + count = quisk_dDecimate(dsamples, count, &filtDecim, quisk_sound_state.mic_sample_rate / n_speech_sample_rate); + else if (quisk_sound_state.mic_sample_rate == 8000 && n_speech_sample_rate != 8000) + QuiskPrintf("Failure to convert input rate in tx_filter_freedv\n"); + // Measure average peak input audio level and limit + dtmp = 1.0 / n_speech_sample_rate; // sample time + time_long = 1.0 - exp(- dtmp / 3.000); + time_short = 1.0 - exp(- dtmp / 0.005); + for (i = 0; i < count; i++) { + dsample = dsamples[i]; + magn = fabs(dsample); + if (magn > inMax) + inMax = inMax * (1 - time_short) + time_short * magn; + else if(magn > mic_agc_level) + inMax = inMax * (1 - time_long) + time_long * magn; + else + inMax = inMax * (1 - time_long) + time_long * mic_agc_level; + dsample = dsample / inMax * Xmin * 0.7; + magn = fabs(dsample); + if (magn < Xmin) + dsamples[i] = dsample; + else if (magn > Xmax) + dsamples[i] = copysign(Ymax, dsample); + else + dsamples[i] = copysign(aaa * magn * magn + bbb * magn + ccc, dsample); + dsamples[i] = dsamples[i] * CLIP16; + } + //QuiskWavWriteD(&hWav, dsamples, count); + if (encode && pt_quisk_freedv_tx) // Encode audio into digital modulation + count = (* pt_quisk_freedv_tx)(filtered, dsamples, count); + //quisk_calc_audio_graph(CLIP16, filtered, NULL, count, 0); + if (filter2.cSamples) // convert float samples to complex with an analytic filter + count = quisk_cCDecimate(filtered, count, &filter2, 1); + // Interpolate up to 48000 + if (MIC_OUT_RATE != n_modem_sample_rate ) + count = quisk_cInterpolate(filtered, count, &cfiltInterp, MIC_OUT_RATE / n_modem_sample_rate); + return count; +} + +PyObject * quisk_get_tx_filter(PyObject * self, PyObject * args) +{ // return the TX filter response to display on the graph +// This is for debugging. Change quisk.py to call QS.get_tx_filter() instead +// of QS.get_filter(). + int i, j, k; + int freq, time; + PyObject * tuple2; + complex double cx; + double scale; + double * average, * fft_window, * bufI, * bufQ; + fftw_complex * samples, * pt; // complex data for fft + fftw_plan plan; // fft plan + double phase, delta; + int nTaps = 325; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + + // Create space for the fft of size data_width + pt = samples = (fftw_complex *) fftw_malloc(sizeof(fftw_complex) * data_width); + plan = fftw_plan_dft_1d(data_width, pt, pt, FFTW_FORWARD, FFTW_MEASURE); + average = (double *) malloc(sizeof(double) * (data_width + nTaps)); + fft_window = (double *) malloc(sizeof(double) * data_width); + bufI = (double *) malloc(sizeof(double) * nTaps); + bufQ = (double *) malloc(sizeof(double) * nTaps); + + for (i = 0, j = -data_width / 2; i < data_width; i++, j++) // Hanning + fft_window[i] = 0.5 + 0.5 * cos(2. * M_PI * j / data_width); + + for (i = 0; i < data_width + nTaps; i++) + average[i] = 0.5; // Value for freq == 0 + for (freq = 1; freq < data_width / 2.0 - 10.0; freq++) { + //freq = data_width * 0.2 / 48.0; + delta = 2 * M_PI / data_width * freq; + phase = 0; + // generate some initial samples to fill the filter pipeline + for (time = 0; time < data_width + nTaps; time++) { + average[time] += cos(phase); // current sample + phase += delta; + if (phase > 2 * M_PI) + phase -= 2 * M_PI; + } + } + // now filter the signal using the transmit filter + tx_filter(NULL, 0); // initialize + scale = 1.0; + for (i = 0; i < data_width; i++) + if (fabs(average[i + nTaps]) > scale) + scale = fabs(average[i + nTaps]); + scale = CLIP16 / scale; // limit to CLIP16 + for (i = 0; i < nTaps; i++) + samples[i] = average[i] * scale; + tx_filter(samples, nTaps); // process initial samples + for (i = 0; i < data_width; i++) + samples[i] = average[i + nTaps] * scale; + tx_filter(samples, data_width); // process the samples + + for (i = 0; i < data_width; i++) // multiply by window + samples[i] *= fft_window[i]; + fftw_execute(plan); // Calculate FFT + // Normalize and convert to log10 + scale = 0.3 / data_width / scale; + for (k = 0; k < data_width; k++) { + cx = samples[k]; + average[k] = cabs(cx) * scale; + if (average[k] <= 1e-7) // limit to -140 dB + average[k] = -7; + else + average[k] = log10(average[k]); + } + // Return the graph data + tuple2 = PyTuple_New(data_width); + i = 0; + // Negative frequencies: + for (k = data_width / 2; k < data_width; k++, i++) + PyTuple_SetItem(tuple2, i, PyFloat_FromDouble(20.0 * average[k])); + + // Positive frequencies: + for (k = 0; k < data_width / 2; k++, i++) + PyTuple_SetItem(tuple2, i, PyFloat_FromDouble(20.0 * average[k])); + + free(bufQ); + free(bufI); + free(average); + free(fft_window); + fftw_destroy_plan(plan); + fftw_free(samples); + + return tuple2; +} + +// Send samples using the Metis-Hermes protocol. A frame is 8 bytes: L/R audio and I/Q mic samples. +// All samples are 2 bytes. The 1032 byte UDP packet contains 63*2 radio sound samples, and 63*2 mic I/Q samples. +// Samples are sent synchronously with the input samples. + +static void quisk_hermes_tx_reset(void) +{ // Reset the buffer to half full of zero samples + int i; + + hermes_num_samples = HERMES_TX_BUF_SAMPLES / 2; + hermes_read_index = 0; + hermes_write_index = hermes_num_samples * 2; + for (i = 0; i < HERMES_TX_BUF_SHORTS; i++) // Put zero mic samples into the buffer + hermes_buf[i] = 0; + //printf("quisk_hermes_tx_reset: hermes_num_samples %d\n", hermes_num_samples); +} + +static void quisk_hermes_tx_add(complex double * cSamples, int tx_count, int key_down) +{ // Add samples to the Tx buffer. + int i; + static int hermes_buf_has_samples=1; + + if (key_down) { // add non-zero samples to buffer + hermes_buf_has_samples = 1; + } + else if (hermes_buf_has_samples) { // key is not down; reset buffer to zero + quisk_hermes_tx_reset(); + hermes_buf_has_samples = 0; + return; + } + else { // key is not down but buffer is zeroed; just reset pointers + hermes_num_samples = HERMES_TX_BUF_SAMPLES / 2; + hermes_read_index = 0; + hermes_write_index = hermes_num_samples * 2; + return; + } + //printf("hermes_tx_add start: hermes_num_samples %d, tx_count %d\n", hermes_num_samples, tx_count); + if (hermes_num_samples + tx_count >= HERMES_TX_BUF_SAMPLES) { // no more space; throw away half the samples + quisk_udp_mic_error("Tx hermes buffer overflow"); + //printf("Tx hermes buffer overflow: hermes_num_samples %d tx_count %d\n", hermes_num_samples, tx_count); + i = hermes_num_samples - HERMES_TX_BUF_SAMPLES / 2; // number of samples to remove + hermes_num_samples -= i; + hermes_read_index += i * 2; + if (hermes_read_index >= HERMES_TX_BUF_SHORTS) + hermes_read_index -= HERMES_TX_BUF_SHORTS; + } + hermes_num_samples += tx_count; + for (i = 0; i < tx_count; i++) { // Put transmit mic samples into the buffer + hermes_buf[hermes_write_index++] = (short)cimag(cSamples[i]); + hermes_buf[hermes_write_index++] = (short)creal(cSamples[i]); + if (hermes_write_index >= HERMES_TX_BUF_SHORTS) + hermes_write_index = 0; + } + //printf ("Buffer usage %.1f %%, hermes_num_samples %d, tx_count %d\n", 100.0 * hermes_num_samples / HERMES_TX_BUF_SAMPLES, hermes_num_samples, tx_count); + //printf("hermes_tx_add end: hermes_num_samples %d\n", hermes_num_samples); +} + +void quisk_hermes_tx_send(int tx_socket, int * tx_records) +{ // Send one UDP block of mic samples using the Metis-Hermes protocol. Timing is from blocks received, rate is 48k. + // If the key is up we send samples anyway, but the samples are zero. + int i, j, offset, sent, ratio; + short s; + unsigned char sendbuf[1032]; + unsigned char * pt_buf; + static unsigned int seq = 0; + static unsigned char C0_index = 0; + complex double cw_samples[63 * 2]; + static double writequeue_time0 = 0; + + //printf("hermes_tx_send start 1: hermes_num_samples %d\n", hermes_num_samples); + if (tx_records == NULL) { + seq = 0; + C0_index = 0; + quisk_hermes_tx_reset(); + return; + } + if (quisk_play_state != last_play_state) { + last_play_state = quisk_play_state; + serial_key_samples(NULL, 0); + } + //printf("hermes_tx_send start 2: hermes_num_samples %d\n", hermes_num_samples); + ratio = quisk_sound_state.sample_rate / 48000; // send rate is 48 ksps + //printf ("quisk_hermes_tx_send ratio %d count %d\n", ratio, *tx_records); + if (*tx_records / ratio < 63 * 2) // tx_records is the number of samples received for each receiver + return; + // Send 63*2 Tx samples with control bytes + if (quisk_play_state == SOFTWARE_CWKEY) { // Send CW samples + serial_key_samples(cw_samples, 63 * 2); + j = 0; + for (i = 0; i < 63 * 2; i++) { + hermes_buf[j++] = (short)cimag(cw_samples[i]); + hermes_buf[j++] = (short)creal(cw_samples[i]); + } + hermes_read_index = 0; + hermes_num_samples = 63 * 2; + } + //printf ("Buffer usage %.1f %%\n", 100.0 * hermes_num_samples / HERMES_TX_BUF_SAMPLES); + //printf ("Tx quisk_hermes_tx_send ratio %d, count %d, samples %d\n", ratio, *tx_records, hermes_num_samples); + *tx_records -= 63 * 2 * ratio; + if (hermes_num_samples < 63 * 2) { // Not enough samples to send + //printf("Tx hermes buffer underflow: hermes_num_samples %d\n", hermes_num_samples); + quisk_udp_mic_error("Tx hermes buffer underflow"); + quisk_hermes_tx_reset(); + } + hermes_num_samples -= 63 * 2; + sendbuf[0] = 0xEF; + sendbuf[1] = 0xFE; + sendbuf[2] = 0x01; + sendbuf[3] = 0x02; + sendbuf[4] = seq >> 24 & 0xFF; + sendbuf[5] = seq >> 16 & 0xFF; + sendbuf[6] = seq >> 8 & 0xFF; + sendbuf[7] = seq & 0xFF; + seq++; + sendbuf[8] = 0x7F; + sendbuf[9] = 0x7F; + sendbuf[10] = 0x7F; + offset = C0_index * 4; // offset into quisk_pc_to_hermes is C0[7:1] * 4 + sendbuf[11] = C0_index << 1 | hermes_mox_bit; // C0 + sendbuf[12] = quisk_pc_to_hermes[offset++]; // C1 + sendbuf[13] = quisk_pc_to_hermes[offset++]; // C2 + sendbuf[14] = quisk_pc_to_hermes[offset++]; // C3 + sendbuf[15] = quisk_pc_to_hermes[offset++]; // C4 + if (C0_index == 0) { // Do not change receiver count without stopping Hermes and restarting + sendbuf[15] = quisk_multirx_count << 3 | 0x04; // Send the old count, not the changed count + if (hermes_mox_bit) // send filter selection on J16 + sendbuf[13] = hermes_filter_tx << 1; + else + sendbuf[13] = hermes_filter_rx << 1; + } + else if ( ! quisk_is_vna && C0_index == 9) { + } + if (++C0_index > 16) + C0_index = 0; + pt_buf = sendbuf + 16; + for (i = 0; i < 63; i++) { // add 63 samples + *pt_buf++ = 0x00; // Left/Right audio sample + *pt_buf++ = 0x00; + *pt_buf++ = 0x00; + *pt_buf++ = 0x00; + s = hermes_buf[hermes_read_index++]; + *pt_buf++ = (s >> 8) & 0xFF; // Two bytes of I + *pt_buf++ = s & 0xFF; + s = hermes_buf[hermes_read_index++]; + *pt_buf++ = (s >> 8) & 0xFF; // Two bytes of Q + *pt_buf++ = s & 0xFF; + if (hermes_read_index >= HERMES_TX_BUF_SHORTS) + hermes_read_index = 0; + } + sendbuf[520] = 0x7F; + sendbuf[521] = 0x7F; + sendbuf[522] = 0x7F; + + // Changes for HermesLite v2 thanks to Steve, KF7O + // Add a delay between ACK requests so two successive requests are spaced. + if (writequeue_time0 == 0) + writequeue_time0 = QuiskTimeSec() + 0.050; // initial delay + if (quisk_hermeslite_writepointer == 1 && QuiskTimeSec() - writequeue_time0 > 0.020) { + writequeue_time0 = QuiskTimeSec(); + // Only send periodic hermeslite writes in second part of frame + sendbuf[523] = quisk_hermeslite_writequeue[0] << 1 | hermes_mox_bit; + sendbuf[524] = quisk_hermeslite_writequeue[1]; + sendbuf[525] = quisk_hermeslite_writequeue[2]; + sendbuf[526] = quisk_hermeslite_writequeue[3]; + sendbuf[527] = quisk_hermeslite_writequeue[4]; + + if ((sendbuf[523] & 0x80) == 0) { + // No acknowledge requested so fire and forget + quisk_hermeslite_writepointer = 0; + } + else { + quisk_hermeslite_writepointer = 2; + } + } else { + offset = C0_index * 4; // offset into quisk_pc_to_hermes is C0[7:1] * 4 + sendbuf[523] = C0_index << 1 | hermes_mox_bit; // C0 + sendbuf[524] = quisk_pc_to_hermes[offset++]; // C1 + sendbuf[525] = quisk_pc_to_hermes[offset++]; // C2 + sendbuf[526] = quisk_pc_to_hermes[offset++]; // C3 + sendbuf[527] = quisk_pc_to_hermes[offset++]; // C4 + if (C0_index == 0) { + sendbuf[527] = quisk_multirx_count << 3 | 0x04; // Send the old count, not the changed count + if (hermes_mox_bit) // send filter selection on J16 + sendbuf[525] = hermes_filter_tx << 1; + else + sendbuf[525] = hermes_filter_rx << 1; + } + else if ( ! quisk_is_vna && C0_index == 9) { + } + if (++C0_index > 16) + C0_index = 0; + } + + pt_buf = sendbuf + 528; + for (i = 0; i < 63; i++) { // add 63 samples + *pt_buf++ = 0x00; // Left/Right audio sample + *pt_buf++ = 0x00; + *pt_buf++ = 0x00; + *pt_buf++ = 0x00; + s = hermes_buf[hermes_read_index++]; + *pt_buf++ = (s >> 8) & 0xFF; // Two bytes of I + *pt_buf++ = s & 0xFF; + s = hermes_buf[hermes_read_index++]; + *pt_buf++ = (s >> 8) & 0xFF; // Two bytes of Q + *pt_buf++ = s & 0xFF; + if (hermes_read_index >= HERMES_TX_BUF_SHORTS) + hermes_read_index = 0; + } + sent = send(tx_socket, (char *)sendbuf, 1032, 0); + if (sent != 1032) + quisk_udp_mic_error("Tx UDP socket error in Hermes"); + if (quisk_play_state == SOFTWARE_CWKEY) + quisk_hermes_tx_reset(); + //printf("hermes_tx_send end: hermes_num_samples %d\n", hermes_num_samples); +} + +// udp_iq has an initial zero followed by the I/Q samples. +// The initial zero is sent iff align4 == 1. + +static void transmit_udp(complex double * cSamples, int count) +{ // Send count samples using the HiQSDR protocol. Each sample is sent as two shorts (4 bytes) of I/Q data. + // Transmission is delayed until a whole block of data is available. + int i, sent; + static short udp_iq[TX_BLOCK_SHORTS + 1] = {0}; + static int udp_size = 1; + + if (mic_socket == INVALID_SOCKET) + return; + if ( ! cSamples) { // initialization + udp_size = 1; + udp_iq[0] = 0; // should not be necessary + return; + } + if (doTxCorrect) { + for (i = 0; i < count; i++) + cSamples[i] = cSamples[i] * TxCorrectLevel + TxCorrectDc; + } + for (i = 0; i < count; i++) { // transmit samples + udp_iq[udp_size++] = (short)creal(cSamples[i]); + udp_iq[udp_size++] = (short)cimag(cSamples[i]); + if (udp_size >= TX_BLOCK_SHORTS) { // check count + if (align4) + sent = send(mic_socket, (char *)udp_iq, udp_size * 2, 0); + else + sent = send(mic_socket, (char *)udp_iq + 1, --udp_size * 2, 0); + if (sent != udp_size * 2) + QuiskPrintf("Send socket returned %d\n", sent); + udp_size = 1; + } + } +} + +static void transmit_mic_carrier(complex double * cSamples, int count, double level) +{ // send a CW carrier instead of mic samples +#if 1 + // transmit a carrier equal to the number of samples + int i; + for (i = 0; i < count; i++) + cSamples[i] = level * CLIP16; +#else + // replace mic samples with a sin wave + int i; + double freq; + complex double phase1; // Phase increment + static complex double vector1 = CLIP16 / 2; + + // Use the sidetone slider 0 to 1000 to set frequency + freq = quisk_sidetoneCtrl * 5; + freq = ((int)freq / 50) * 50; + phase1 = cexp(I * 2.0 * M_PI * freq / 48000.0); + //phase1 = conj(phase1); + for (i = 0; i < count; i++) { + vector1 *= phase1; + cSamples[i] = vector1 * level; + } +#endif +} + +static void transmit_mic_imd(complex double * cSamples, int count, double level) +{ // send a 2-tone test signal instead of mic samples + int i; + complex double v; + static complex double phase1=0, phase2; // Phase increment + static complex double vector1; + static complex double vector2; + + if (phase1 == 0) { // initialize + phase1 = cexp((I * 2.0 * M_PI * IMD_TONE_1) / MIC_OUT_RATE); + phase2 = cexp((I * 2.0 * M_PI * IMD_TONE_2) / MIC_OUT_RATE); + vector1 = CLIP16 / 2.0; + vector2 = CLIP16 / 2.0; + } + for (i = 0; i < count; i++) { // transmit a carrier equal to the number of samples + vector1 *= phase1; + vector2 *= phase2; + v = level * (vector1 + vector2); + cSamples[i] = v; + } +} + +int quisk_process_microphone(int mic_sample_rate, complex double * cSamples, int count) +{ + int i, sample, maximum, interp, mic_interp, key_down; + double d, ctcss_delta, audio_scale, ctcss_scale; + static int key_was_down=0, key_down_counter=0; + static struct quisk_cFilter filtInterp={NULL}; + static struct quisk_cFilter filtInterp2={NULL}; + static struct quisk_cFilter filt240D4 = {NULL}; + static struct quisk_cFilter filt300D6 = {NULL}; + static struct quisk_cHB45Filter HalfBand = {NULL, 0, 0}; + static double ctcss_angle=0; + static struct alc tx_alc = {NULL}; + +#if 0 + // Measure soundcard actual sample rate + static time_t seconds = 0; + static int total = 0; + struct timeval tb; + static double dtime; + + gettimeofday(&tb); + total += count; + if (seconds == 0) { + seconds = tb.tv_sec; + dtime = tb.tv_sec + 0.000001 * tb.tv_usec; + } + else if (tb.tv_sec - seconds > 4) { + printf("Mic soundcard rate %.3f\n", total / (tb.tv_sec + .000001 * tb.tv_usec - dtime)); + seconds = tb.tv_sec; + printf("backlog %d, count %d\n", backlog, count); + } +#endif + +#if DEBUG_IO || DEBUG + //QuiskPrintTime("", -1); +#endif + +#if DEBUG_IO || DEBUG + double magn; + static double out_max=0; +#endif +#if DEBUG_LEVEL || DEBUG_IO || DEBUG + debug_timer += count; + if (debug_timer >= mic_sample_rate) // one second + debug_timer = 0; +#endif + +// Microphone sample are input at mic_sample_rate. But after processing, +// the output rate is MIC_OUT_RATE. + interp = MIC_OUT_RATE / mic_sample_rate; + +#if USE_GET_SIN + get_sin(cSamples, count); // Replace mic samples with a sin wave +#endif +#if USE_2TONE + get_2tone(cSamples, count); // Replace mic samples with a 2-tone test signal +#endif + // measure maximum microphone level + maximum = 1; + for (i = 0; i < count; i++) { + cSamples[i] *= (double)CLIP16 / CLIP32; // convert 32-bit samples to 16 bits + d = creal(cSamples[i]); + sample = (int)fabs(d); + if (sample > maximum) + maximum = sample; + } + // VOX processing + if (maximum > vox_level) { + is_vox = mic_sample_rate / 1000 * timeVOX; // reset timer to maximum + } + else if(is_vox) { + is_vox -= count; // decrement timer + if (is_vox < 0) + is_vox = 0; + } + // mic display level + if (maximum > mic_level) + mic_level = maximum; + mic_timer -= count; // time out the max microphone level to display + if (mic_timer <= 0) { + mic_timer = mic_sample_rate / 1000 * MIC_MAX_HOLD_TIME; + mic_max_display = mic_level; + mic_level = 1; + } + + if ( ! tx_alc.buffer) + init_alc(&tx_alc, 960); // at 48000 sps, 960 is 20 msec + + // quiskTxHoldState is a state machine to implement a pause for a repeater frequency shift for FM + key_down = quisk_is_key_down(); + if (rxMode == FM || rxMode == DGT_FM) { + switch (quiskTxHoldState) { + case 0: // Never implement any hold + break; + case 1: // Start hold when key goes down + if (key_down) + quiskTxHoldState = 2; + break; + case 2: // Key down hold is in progress; wait until state changes to 3 + break; + case 3: // Hold is released; when key goes up, hold starts again + if ( ! key_down) + quiskTxHoldState = 4; + break; + case 4: // Key up hold is in progress; wait until state changes to 1 + break; + } + } + if (quiskTxHoldState == 2 || quiskTxHoldState == 4) { // don't transmit until the hold is cleared + key_down = 0; + for (i = 0; i < count; i++) + cSamples[i] = 0; + } + if (key_down != key_was_down) { + key_down_counter = quisk_start_ssb_delay * 48; // Key was pressed. Zero the first few samples to clear the buffers. + init_alc(&tx_alc, 0); // init ALC for Tx and TMP_RECORD_MIC + key_was_down = key_down; + } + if (quisk_play_state != last_play_state) { + last_play_state = quisk_play_state; + serial_key_samples(NULL, 0); + } + if (key_down || DEBUG_MIC) { // create transmit I/Q samples +#if USE_GET_SIN == 2 + transmit_udp(cSamples, count * interp); +#else + if (quiskSpotLevel >= 0) { // Spot is in use + count *= interp; + transmit_mic_carrier(cSamples, count, quiskSpotLevel / 1000.0); + } + else switch (rxMode) { + case LSB: // LSB + case USB: // USB + if (quisk_record_state == TMP_PLAY_SPKR_MIC) + count = tx_filter_digital(cSamples, count); // filter samples, minimal processing + else + count = tx_filter(cSamples, count); // filter samples + process_alc(cSamples, count, &tx_alc, rxMode); + break; + case AM: // AM + if (quisk_record_state != TMP_PLAY_SPKR_MIC) // no audio processing for recorded sound + count = tx_filter(cSamples, count); + for (i = 0; i < count; i++) // transmit (0.5 + ampl/2, 0) + cSamples[i] = (creal(cSamples[i]) + CLIP16) * 0.5; + process_alc(cSamples, count, &tx_alc, rxMode); + break; + case FM: // FM + if (quisk_record_state != TMP_PLAY_SPKR_MIC) // no audio processing for recorded sound + count = tx_filter(cSamples, count); + if (quisk_ctcss_freq > 9) { + ctcss_delta = 2.0 * M_PI / MIC_OUT_RATE * quisk_ctcss_freq; + ctcss_scale = 450.0 * modulation_index / quisk_ctcss_freq; // for 15% of total deviation + audio_scale = 0.85 * modulation_index / CLIP16; + for (i = 0; i < count; i++) { + ctcss_angle += ctcss_delta; + if (ctcss_angle >= 2.0 * M_PI) + ctcss_angle -= 2.0 * M_PI; + cSamples[i] = CLIP16 * cexp(I * (audio_scale * creal(cSamples[i]) + ctcss_scale * sin (ctcss_angle))); + } + } + else { + audio_scale = modulation_index / CLIP16; + for (i = 0; i < count; i++) // this is phase modulation == FM and 6 dB /octave preemphasis + cSamples[i] = CLIP16 * cexp(I * audio_scale * creal(cSamples[i])); + } + process_alc(cSamples, count, &tx_alc, rxMode); + break; + case DGT_U: // external digital modes + case DGT_L: + case DGT_IQ: + case DGT_FM: + count = tx_filter_digital(cSamples, count); // filter samples, minimal processing + process_alc(cSamples, count, &tx_alc, rxMode); + break; + case IMD: // transmit IMD 2-tone test + count *= interp; + transmit_mic_imd(cSamples, count, quiskImdLevel / 1000.0); + break; + case FDV_U: // FDV + case FDV_L: + count = tx_filter_freedv(cSamples, count, 1); + process_alc(cSamples, count, &tx_alc, rxMode); + break; + default: + break; + } +#if DEBUG_IO || DEBUG + for (i = 0; i < count; i++) { + magn = cabs(cSamples[i]); + if (out_max < magn) + out_max = magn; + } + if (debug_timer == 0) { + printf("Max cSamples[] Output Level %9.6f\n", out_max / CLIP16); + out_max = 0; + } +#endif + if (key_down_counter > 0) { // zero the first few samples to clear the buffers. + for (i = 0; i < count && key_down_counter > 0; i++, key_down_counter--) { + if (key_down_counter < 480) + cSamples[i] *= (1.0 - key_down_counter / 480.0); // slow increase at end + else + cSamples[i] = 0; // initial samples are zero + } + } +#endif + } + if (reverse_tx_sideband) + for (i = 0; i < count; i++) + cSamples[i] = conj(cSamples[i]); + if (quisk_use_rx_udp == 10) + ; // hermes handles this itself + else if (quisk_play_state == SOFTWARE_CWKEY) + serial_key_samples(cSamples, count); + if(quisk_pt_sample_write) { // Used for SoapySDR + // Interpolate the mic samples to the Tx sample rate +//printf("Tx sample rate %i\n", tx_sample_rate); + switch (tx_sample_rate) { + case 100000: + count = quisk_cInterp2HB45(cSamples, count, &HalfBand); + // Fall through + case 50000: + if (! filt240D4.dCoefs) + quisk_filt_cInit(&filt240D4, quiskFilt240D4Coefs, sizeof(quiskFilt240D4Coefs)/sizeof(double)); + if (! filt300D6.dCoefs) + quisk_filt_cInit(&filt300D6, quiskFilt300D6Coefs, sizeof(quiskFilt300D6Coefs)/sizeof(double)); + count = quisk_cInterpDecim(cSamples, count, &filt240D4, 5, 4); // 60 kSps + count = quisk_cInterpDecim(cSamples, count, &filt300D6, 5, 6); // 50 kSps + break; + case 96000: + if (! filtInterp2.dCoefs) + quisk_filt_cInit(&filtInterp2, quiskFilt48dec24Coefs, sizeof(quiskFilt48dec24Coefs)/sizeof(double)); + count = quisk_cInterpolate(cSamples, count, &filtInterp2, 2); + break; + case 192000: + if (! filtInterp2.dCoefs) + quisk_filt_cInit(&filtInterp2, quiskFilt48dec24Coefs, sizeof(quiskFilt48dec24Coefs)/sizeof(double)); + count = quisk_cInterp2HB45(cSamples, count, &HalfBand); + count = quisk_cInterpolate(cSamples, count, &filtInterp2, 2); + break; + default: // 48000 + break; + } + (*quisk_pt_sample_write)(cSamples, count); + } + else if (quisk_use_rx_udp == 10) { // Send Hermes mic samples when key is up or down + if ( ! quisk_rx_udp_started) + ; + else { + if ((rxMode == CWL || rxMode == CWU) && quiskSpotLevel < 0) // CW and no Spot + for (i = 0; i < count; i++) + cSamples[i] = 0; + quisk_hermes_tx_add(cSamples, count, key_down); + } + } + else if (quisk_use_rx_udp && key_down) { // Send mic samples to UDP when key is down + transmit_udp(cSamples, count); + } + if (quisk_record_state == TMP_RECORD_MIC) { + switch (rxMode) { + case LSB: // LSB + case USB: // USB + count = tx_filter(cSamples, count); // filter samples + process_alc(cSamples, count, &tx_alc, rxMode); + break; + case AM: // AM + count = tx_filter(cSamples, count); + process_alc(cSamples, count, &tx_alc, rxMode); + break; + case FM: // FM + count = tx_filter(cSamples, count); + process_alc(cSamples, count, &tx_alc, rxMode); + break; + case FDV_U: // FDV + case FDV_L: + count = tx_filter_freedv(cSamples, count, 0); + process_alc(cSamples, count, &tx_alc, rxMode); + break; + default: + for (i = 0; i < count; i++) + cSamples[i] = 0; + break; + } + // Perhaps interpolate the mic samples back to the sound play rate + mic_interp = quisk_sound_state.playback_rate / MIC_OUT_RATE; + if (mic_interp > 1) { + if (! filtInterp.dCoefs) + quisk_filt_cInit(&filtInterp, quiskFilt12_19Coefs, sizeof(quiskFilt12_19Coefs)/sizeof(double)); + count = quisk_cInterpolate(cSamples, count, &filtInterp, mic_interp); + } + quisk_tmp_record(cSamples, count, (double)CLIP32 / CLIP16); // convert 16 to 32 bits + } + +#if DEBUG_IO || DEBUG + //QuiskPrintTime(" process_mic", 1); +#endif + return count; +} + +PyObject * quisk_set_tx_audio(PyObject * self, PyObject * args, PyObject * keywds) +{ /* Call with keyword arguments ONLY; change Tx audio parameters */ + static char * kwlist[] = {"vox_level", "vox_time", "mic_clip", "mic_preemphasis", "tx_sample_rate", + "reverse_tx_sideband", NULL} ; + int vlevel = -9999, clevel = -9999; + + if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiidii", kwlist, + &vlevel, &timeVOX, &clevel, &quisk_mic_preemphasis, &tx_sample_rate, &reverse_tx_sideband)) + return NULL; + if (vlevel != -9999) + vox_level = (int)(pow(10.0, vlevel / 20.0) * CLIP16); // Convert dB to 16-bit sample + if (clevel != -9999) + quisk_mic_clip = pow(10.0, clevel / 20.0); // Convert dB to factor + Py_INCREF (Py_None); + return Py_None; +} + +PyObject * quisk_set_udp_tx_correct(PyObject * self, PyObject * args) // Called from GUI thread +{ + double DcI, DcQ, level; + + if (!PyArg_ParseTuple (args, "ddd", &DcI, &DcQ, &level)) + return NULL; + if (DcI == 0 && DcQ == 0 && level == 1.0){ + doTxCorrect = 0; + } + else { + doTxCorrect = 1; + TxCorrectDc = (DcI + I * DcQ) * CLIP16; + DcI = fabs(DcI); + DcQ = fabs(DcQ); + if (DcI > DcQ) + TxCorrectLevel = 1.0 - DcI; + else + TxCorrectLevel = 1.0 - DcQ; + TxCorrectLevel *= level; + } + Py_INCREF (Py_None); + return Py_None; +} + +PyObject * quisk_is_vox(PyObject * self, PyObject * args) +{ /* return the VOX state */ + if (!PyArg_ParseTuple (args, "")) + return NULL; + return PyInt_FromLong(is_vox); +} + +PyObject * quisk_set_hermes_filter(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "ii", &hermes_filter_rx, &hermes_filter_tx)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +void quisk_close_mic(void) +{ + if (mic_socket != INVALID_SOCKET) { + close(mic_socket); + mic_socket = INVALID_SOCKET; + } +#ifdef MS_WINDOWS + if (mic_cleanup) + WSACleanup(); +#endif +} + +void quisk_open_mic(void) +{ + struct sockaddr_in Addr; + int sndsize = 48000; +#if DEBUG_IO || DEBUG + int intbuf; +#ifdef MS_WINDOWS + int bufsize = sizeof(int); +#else + socklen_t bufsize = sizeof(int); +#endif +#endif + +#ifdef MS_WINDOWS + WORD wVersionRequested; + WSADATA wsaData; +#endif + + modulation_index = QuiskGetConfigDouble("modulation_index", 1.6); + mic_agc_level = QuiskGetConfigDouble("mic_agc_level", 0.1); + if (quisk_sound_state.tx_audio_port == 0x553B) + align4 = 0; // Using old port: data starts at byte 42. + else + align4 = 1; // Start data at byte 44; align to dword + if (quisk_sound_state.mic_ip[0]) { +#ifdef MS_WINDOWS + wVersionRequested = MAKEWORD(2, 2); + if (WSAStartup(wVersionRequested, &wsaData) != 0) + return; // failure to start winsock + mic_cleanup = 1; +#endif + mic_socket = socket(PF_INET, SOCK_DGRAM, 0); + if (mic_socket != INVALID_SOCKET) { + setsockopt(mic_socket, SOL_SOCKET, SO_SNDBUF, (char *)&sndsize, sizeof(sndsize)); + Addr.sin_family = AF_INET; +// This is the UDP port for TX microphone samples, and must agree with the microcontroller. + Addr.sin_port = htons(quisk_sound_state.tx_audio_port); +#ifdef MS_WINDOWS + Addr.sin_addr.S_un.S_addr = inet_addr(quisk_sound_state.mic_ip); +#else + inet_aton(quisk_sound_state.mic_ip, &Addr.sin_addr); +#endif + if (connect(mic_socket, (const struct sockaddr *)&Addr, sizeof(Addr)) != 0) { + close(mic_socket); + mic_socket = INVALID_SOCKET; + } + else { +#if DEBUG_IO || DEBUG + if (getsockopt(mic_socket, SOL_SOCKET, SO_SNDBUF, (char *)&intbuf, &bufsize) == 0) + printf("UDP mic socket send buffer size %d\n", intbuf); + else + printf ("Failure SO_SNDBUF\n"); +#endif + } + } + } +} + +void quisk_set_tx_mode(void) // called when the mode rxMode is changed +{ + tx_filter(NULL, 0); + tx_filter_digital(NULL, 0); + transmit_udp(NULL, 0); + tx_filter_freedv(NULL, 0, 0); + serial_key_samples(NULL, 0); +} + +#define CW_RISE_MSEC 5 +#define CW_DELAY_SAMPLES (START_CW_DELAY_MAX * 48) +static void serial_key_samples(complex double * cSamples, int count) // called from the sound thread +{ // Internal CW from serial port key - fill cSamples with CW samples + int i, key_down, delay; + static double ampl = 0; + static char delay_line[CW_DELAY_SAMPLES]; // play CW a fixed delay after the key + static int delay_index; + double delta = 1.0 / (CW_RISE_MSEC * 48.0); + double themax = 1.0; + + if ( ! cSamples) { // initialize + for (i = 0; i < CW_DELAY_SAMPLES; i++) + delay_line[i] = 0; + delay_index = 0; + return; + } + + delay = quisk_start_cw_delay * 48; + for (i = 0; i < count; i++) { + key_down = delay_line[delay_index]; + delay_line[delay_index] = QUISK_CWKEY_DOWN; + if (++delay_index >= delay) { + delay_index = 0; + } + if (key_down) { // key is down + if (ampl < themax) { + ampl += delta; + if (ampl > themax) + ampl = themax; + } + } + else { // key is up + if (ampl > 0.0) { + ampl -= delta; + if (ampl < 0.0) + ampl = 0.0; + } + } + cSamples[i] = ampl * CLIP16; + } +} diff --git a/microphone.h b/microphone.h new file mode 100644 index 0000000..517bd15 --- /dev/null +++ b/microphone.h @@ -0,0 +1 @@ +// Filters were moved to filter.c. diff --git a/midi_handler.py b/midi_handler.py new file mode 100644 index 0000000..b02c5f0 --- /dev/null +++ b/midi_handler.py @@ -0,0 +1,161 @@ +# This module implements Midi processing in Quisk. The OnReadMIDI() method is called for any Midi bytes +# received. Do not change this file. If you want to replace it with your own Midi handler, create a +# configuration file, copy this file into it and make any changes there. Quisk will use your configuration +# file MidiHandler instead of this one. + +# Midi messages are generally three bytes long. The first byte is the status and has the most significant bit set. +# Subsequent bytes have the most significant bit zero. The status byte of a channel message is a 1 bit, three bits of message +# type and 4 bits of channel number. This is followed by two bytes of data. + +# For Note On (status 0x9?) and Note Off (status 0x8?) the data bytes are the note number and velocity. Velocity +# indicates how hard the key was pressed. If the velocity of a Note On message is zero it is treated the same as Note Off. + +# For a control change (status = 0xB?) the data bytes are the controller number and the controller value. For some controllers +# it only matters if the value is less than 64 (down) or 64 or greater (up). For other controllers the value 0 to 127 +# is the actual control setting. + +import traceback + +class MidiHandler: # Quisk calls this to make the Midi handler instance. + tune_speed = {0:10, 1:20, 2:50, 3:100, 4:200, 5:500, 6:1000, 7:2000, 8:5000, 9:10000} + slider_speed = {0:1, 1:2, 2:3, 3:5, 4:7, 5:9, 6:12, 7:15, 8:18, 9:22} + def __init__(self, app, conf): + self.app = app # The application object + self.conf = conf # The configuration settings + self.midi_message = [] # Save Midi bytes until a whole message is received. + def OnReadMIDI(self, byts): # Quisk calls this for any Midi bytes received. + for byt in byts: + if byt & 0x80: # this is a status byte and the start of a new message + self.midi_message = [byt] + else: + self.midi_message.append(byt) + if len(self.midi_message) == 3: + #print ("0x%2X%02X %d" % tuple(self.midi_message)) + status = self.midi_message[0] + status = status & 0xF0 # Ignore channel + if status == 0x90: # Note On + if self.midi_message[2] == 0: # Note On with zero velocity is the same as Note Off + self.NoteOff() + else: + self.NoteOn() + elif status == 0x80: # Note Off + self.NoteOff() + elif status == 0xB0: # Control Change + try: + name = self.app.local_conf.MidiNoteDict["0x%02X%02X" % (self.midi_message[0], self.midi_message[1])] + except: + pass + #traceback.print_exc() + else: + if len(name) > 3 and name[-3] == " " and name[-2] in "+-" and name[-1] in "0123456789": + self.JogWheel(name) + else: + self.ControlKnob(name) + def NoteOn(self): + try: + name = self.app.local_conf.MidiNoteDict["0x%02X%02X" % (self.midi_message[0], self.midi_message[1])] + btn = self.app.idName2Button[name] + except: + return + if btn.idName == 'PTT' and not self.conf.midi_ptt_toggle: + btn.SetValue(True, True) + else: + btn.Shortcut(None, name) + def NoteOff(self): + try: # Look up the Note On name + name = self.app.local_conf.MidiNoteDict["0x9%X%02X" % (self.midi_message[0] & 0xF, self.midi_message[1])] + btn = self.app.idName2Button[name] + except: + return + if hasattr(btn, "repeat_state"): # This is a QuiskRepeatbutton + btn.Shortcut(None, "_end_") + elif btn.idName == 'PTT' and not self.conf.midi_ptt_toggle: + btn.SetValue(False, True) + def ControlKnob(self, name): + if self.midi_message[2] == 64: # Mid control + dec_value = 0.5 + else: + dec_value = self.midi_message[2] / 127.0 + if name == "Tune": + tune = self.app.sample_rate * (dec_value - 0.5) * 0.98 + tune = int(tune) + self.app.ChangeHwFrequency(tune, self.app.VFO, 'FreqEntry') + elif name == "Rit": # Offset values by the CW tone frequency + ctrl, func = self.app.midiControls[name] + value = self.midi_message[2] - 64 # Center value + if self.app.mode == 'CWU': + offset = - self.conf.cwTone + value = value * 1000 // 63 + offset + elif self.app.mode == 'CWL': + offset = self.conf.cwTone + value = value * 1000 // 63 + offset + else: + offset = 0 + value = value * 2000 // 63 + offset + if value < ctrl.themin: + value = ctrl.themin + elif value > ctrl.themax: + value = ctrl.themax + ctrl.SetValue(value) + if self.app.remote_control_head: + self.app.Hardware.RemoteCtlSend(f'{ctrl.idName};{ctrl.GetValue()}\n') + func() + elif name in self.app.midiControls: + ctrl, func = self.app.midiControls[name] + if ctrl: + ctrl.SetDecValue(dec_value, False) + if self.app.remote_control_head: + self.app.Hardware.RemoteCtlSend(f'{ctrl.idName};{ctrl.GetValue()}\n') + func() + else: # Try to treat as Note On/Off + try: + btn = self.app.idName2Button[name] + except: + return + if self.midi_message[2] == 0: # Note On with zero velocity is the same as Note Off + if hasattr(btn, "repeat_state"): # This is a QuiskRepeatbutton + btn.Shortcut(None, "_end_") + else: # Note On + btn.Shortcut(None, name) + def JogWheel(self, name): + speed = int(name[-1]) + if name[-2] == '+': + direction = +1 + else: + direction = -1 + name = name[0:-3] + if name == "Tune": + freq = self.app.txFreq + self.app.VFO + delta = self.tune_speed[speed] + if self.midi_message[2] < 64: + freq += direction * delta + else: + freq -= direction * delta + freq = ((freq + delta // 2) // delta) * delta + tune = freq - self.app.VFO + d = self.app.sample_rate * 45 // 100 + if -d <= tune <= d: # Frequency is on-screen + vfo = self.app.VFO + else: # Change the VFO + vfo = (freq // 5000) * 5000 - 5000 + tune = freq - vfo + self.app.ChangeHwFrequency(tune, vfo, 'FreqEntry') + elif name in self.app.midiControls: + ctrl, func = self.app.midiControls[name] + self.AdjSlider(ctrl, direction, speed) + if self.app.remote_control_head: + self.app.Hardware.RemoteCtlSend(f'{ctrl.idName};{ctrl.GetValue()}\n') + func() + else: + pass #print ("Unknown jog name", name) + def AdjSlider(self, ctrl, direction, speed): + value = ctrl.GetValue() + if self.midi_message[2] < 64: + value += direction * self.slider_speed[speed] + else: + value -= direction * self.slider_speed[speed] + if value < ctrl.themin: + value = ctrl.themin + elif value > ctrl.themax: + value = ctrl.themax + ctrl.SetValue(value) diff --git a/n2adr/__init__.py b/n2adr/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/n2adr/__init__.py @@ -0,0 +1 @@ +# diff --git a/n2adr/conf1.py b/n2adr/conf1.py new file mode 100644 index 0000000..74b3c95 --- /dev/null +++ b/n2adr/conf1.py @@ -0,0 +1,13 @@ +from n2adr.quisk_conf import n2adr_sound_pc_capt, n2adr_sound_pc_play, n2adr_sound_usb_play, n2adr_sound_usb_mic, favorites_file_path + +settings_file_path = "../quisk_settings.json" + +name_of_sound_play = n2adr_sound_usb_play +microphone_name = n2adr_sound_usb_mic +digital_input_name = "" +digital_output_name = "" + +use_rx_udp = 1 # Get ADC samples from UDP +rx_udp_ip = "192.168.1.196" # Sample source IP address +rx_udp_port = 0xBC77 # Sample source UDP port +graph_width = 0.80 diff --git a/n2adr/conf2.py b/n2adr/conf2.py new file mode 100644 index 0000000..00d2059 --- /dev/null +++ b/n2adr/conf2.py @@ -0,0 +1,20 @@ +# This is a second config file that I use to test various hardware configurations. + +from n2adr.quisk_conf import n2adr_sound_pc_capt, n2adr_sound_pc_play, n2adr_sound_usb_play, n2adr_sound_usb_mic +from n2adr.quisk_conf import latency_millisecs, data_poll_usec, favorites_file_path + +settings_file_path = "../quisk_settings.json" + +name_of_sound_play = n2adr_sound_usb_play +name_of_sound_capt = n2adr_sound_pc_capt + +sdriq_name = "/dev/ttyUSB0" # Name of the SDR-IQ device to open + +default_screen = 'WFall' +waterfall_y_scale = 80 +waterfall_y_zero = 40 +waterfall_graph_y_scale = 40 +waterfall_graph_y_zero = 90 +waterfall_graph_size = 160 +display_fraction = 1.00 # The edges of the full bandwidth are not valid + diff --git a/n2adr/conf3.py b/n2adr/conf3.py new file mode 100644 index 0000000..8dacf47 --- /dev/null +++ b/n2adr/conf3.py @@ -0,0 +1,13 @@ +from n2adr.quisk_conf import n2adr_sound_pc_capt, n2adr_sound_pc_play, n2adr_sound_usb_play, n2adr_sound_usb_mic, favorites_file_path + +name_of_sound_play = n2adr_sound_usb_play +microphone_name = n2adr_sound_usb_mic + +settings_file_path = "../quisk_settings.json" + +rx_udp_clock = 73728000 - 102 +rx_udp_ip = "192.168.1.213" # Sample source IP address "" for DHCP + +do_repeater_offset = True +#bandTransverterOffset = {'10' : 300000} +spot_button_keys_tx = True diff --git a/n2adr/conf3A.py b/n2adr/conf3A.py new file mode 100644 index 0000000..a807766 --- /dev/null +++ b/n2adr/conf3A.py @@ -0,0 +1,5 @@ +from n2adr import conf3 + +settings_file_path = "../quisk_settings.json" + +microphone_name = "" diff --git a/n2adr/conf4.py b/n2adr/conf4.py new file mode 100644 index 0000000..e0d20ae --- /dev/null +++ b/n2adr/conf4.py @@ -0,0 +1,40 @@ +# This is a config file to test the microphone by sending microphone Tx playback to the audio out. +# Set the frequency to zero, and press FDX and PTT. +# Set these values for DEBUG_MIC in sound.c: +# 0: Normal FFT. +# 1: Send filtered Tx audio to the FFT. +# 2: Send mic playback to the FFT. +# 3: Send unfiltered mono mic audio to the FFT. + +import sys +from quisk_hardware_model import Hardware as BaseHardware +import _quisk as QS + +from n2adr.quisk_conf import n2adr_sound_pc_capt, n2adr_sound_pc_play, n2adr_sound_usb_play, n2adr_sound_usb_mic +from n2adr.quisk_conf import latency_millisecs, data_poll_usec, favorites_file_path +from n2adr.quisk_conf import mixer_settings + +settings_file_path = "../quisk_settings.json" + +name_of_sound_capt = n2adr_sound_pc_capt +name_of_sound_play = '' +microphone_name = n2adr_sound_usb_mic +name_of_mic_play = n2adr_sound_usb_play + +graph_y_scale = 160 + +mic_sample_rate = 48000 + +sample_rate = 48000 +mic_playback_rate = 48000 +mic_out_volume = 0.6 +add_fdx_button = 1 + +class Hardware(BaseHardware): + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + def OnButtonPTT(self, event): + if event.GetEventObject().GetValue(): + QS.set_key_down(1) + else: + QS.set_key_down(0) diff --git a/n2adr/conf5.py b/n2adr/conf5.py new file mode 100644 index 0000000..78844e1 --- /dev/null +++ b/n2adr/conf5.py @@ -0,0 +1,15 @@ +import sys + +settings_file_path = "../quisk_settings.json" + +#hamlib_port = 4575 # Standard port for Quisk control. Set the port in Hamlib to 4575 too. +hamlib_port = 4532 # Default port for rig 2. Use this if you can not set the Hamlib port. +if sys.platform == "win32": + pass +elif 0: + digital_input_name = 'pulse' + digital_output_name ='' +else: + digital_input_name = 'hw:Loopback,0' + digital_output_name = digital_input_name + diff --git a/n2adr/conf6.py b/n2adr/conf6.py new file mode 100644 index 0000000..d3ebec7 --- /dev/null +++ b/n2adr/conf6.py @@ -0,0 +1,30 @@ +# This is a second config file to test the softrock radios. + +from n2adr.quisk_conf import n2adr_sound_pc_capt, n2adr_sound_pc_play, n2adr_sound_usb_play, n2adr_sound_usb_mic +from n2adr.quisk_conf import latency_millisecs, data_poll_usec, favorites_file_path +from n2adr.quisk_conf import mixer_settings + +settings_file_path = "../quisk_settings.json" + +name_of_sound_capt = n2adr_sound_pc_capt +name_of_sound_play = n2adr_sound_usb_play + +default_screen = 'WFall' +waterfall_y_scale = 80 +waterfall_y_zero = 40 +waterfall_graph_y_scale = 40 +waterfall_graph_y_zero = 90 +waterfall_graph_size = 160 +display_fraction = 1.00 + +sample_rate = 48000 +playback_rate = 48000 + +do_repeater_offset = True +#bandTransverterOffset = {'40' : 300000} + +# Microphone capture and playback: +microphone_name = n2adr_sound_usb_mic +name_of_mic_play = n2adr_sound_pc_play +mic_playback_rate = sample_rate +mic_out_volume = 0.6 diff --git a/n2adr/conf7.py b/n2adr/conf7.py new file mode 100644 index 0000000..f47309b --- /dev/null +++ b/n2adr/conf7.py @@ -0,0 +1,14 @@ +import sys + +settings_file_path = "../quisk_settings.json" + +if sys.platform == "win32": + digital_output_name = 'CABLE-A Input' +elif 0: + digital_input_name = 'pulse' + digital_output_name ='' +else: + digital_output_name = 'hw:Loopback,0' + +name_of_sound_play = '' +microphone_name = '' diff --git a/n2adr/hl2_hardware.py b/n2adr/hl2_hardware.py new file mode 100644 index 0000000..4ad0ffb --- /dev/null +++ b/n2adr/hl2_hardware.py @@ -0,0 +1,72 @@ +# This is the hardware control file for my shack. +# It is for the Hermes-Lite2 5 watt output which uses only the antenna tuner. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from hermes.quisk_hardware import Hardware as BaseHw +from n2adr import station_hardware + +class Hardware(BaseHw): + def __init__(self, app, conf): + BaseHw.__init__(self, app, conf) + self.GUI = None + self.vfo_frequency = 0 # current vfo frequency + # Other hardware + self.anttuner = station_hardware.AntennaTuner(app, conf) # Control the antenna tuner + self.controlbox = station_hardware.ControlBox(app, conf) # Control my Station Control Box + self.v2filter = station_hardware.FilterBoxV2(app, conf) # Control V2 filter box + def open(self): + if False: + from n2adr.station_hardware import StationControlGUI + self.GUI = StationControlGUI(self.application.main_frame, self, self.application, self.conf) + self.GUI.Show() + self.anttuner.open() + return BaseHw.open(self) + def close(self): + self.anttuner.close() + self.controlbox.close() + return BaseHw.close(self) + def ChangeFilterFrequency(self, tx_freq): + if tx_freq and tx_freq > 0: + if self.GUI: + self.GUI.SetTxFreq(tx_freq) + self.GUI.freq_entry.ChangeValue("%.3f" % (tx_freq * 1E-6)) + else: + self.anttuner.SetTxFreq(tx_freq) + self.v2filter.SetTxFreq(tx_freq) + def ChangeFrequency(self, tx_freq, vfo_freq, source='', band='', event=None): + self.ChangeFilterFrequency(tx_freq) + return BaseHw.ChangeFrequency(self, tx_freq, vfo_freq, source, band, event) + def ChangeBand(self, band): + # band is a string: "60", "40", "WWV", etc. + ret = BaseHw.ChangeBand(self, band) + self.anttuner.ChangeBand(band) + #self.lpfilter.ChangeBand(band) + #self.hpfilter.ChangeBand(band) + self.v2filter.ChangeBand(band) + self.CorrectSmeter() + return ret + def HeartBeat(self): # Called at about 10 Hz by the main + self.anttuner.HeartBeat() + self.v2filter.HeartBeat() + self.controlbox.HeartBeat() + return BaseHw.HeartBeat(self) + def OnSpot(self, level): + # level is -1 for Spot button Off; else the Spot level 0 to 1000. + self.anttuner.OnSpot(level) + return BaseHw.OnSpot(self, level) + def OnButtonRfGain(self, event): + self.v2filter.OnButtonRfGain(event) + self.CorrectSmeter() + def CorrectSmeter(self): # S-meter correction can change with band or RF gain + return + if self.band == '40': # Basic S-meter correction by band + self.correct_smeter = 20.5 + else: + self.correct_smeter = 20.5 + #self.correct_smeter -= self.rf_gain / 6.0 # Correct S-meter for RF gain + #self.application.waterfall.ChangeRfGain(self.rf_gain) # Waterfall colors are constant + def OnButtonPTT(self, event): + self.controlbox.OnButtonPTT(event) + return BaseHw.OnButtonPTT(self, event) diff --git a/n2adr/quisk_conf.py b/n2adr/quisk_conf.py new file mode 100644 index 0000000..58e5d4f --- /dev/null +++ b/n2adr/quisk_conf.py @@ -0,0 +1,47 @@ +# This is the config file from my shack, which controls various hardware. +# The files to control my 2010 transceiver and for the improved version HiQSDR +# are in the package directory HiQSDR. + +import sys + +sample_rate = 48000 + +if sys.platform == 'win32': + favorites_file_path = "C:/pub/quisk_favorites.txt" + settings_file_path = "C:/pub/quisk_settings.json" + name_of_sound_play = "" + name_of_sound_capt = "Primary" + microphone_name = "" + name_of_mic_play = "" +else: + favorites_file_path = "/home/jim/pub/quisk_favorites.txt" + settings_file_path = "/home/jim/pub/quisk_settings.json" + name_of_sound_play = "" + name_of_sound_capt = "hw:0" + microphone_name = "" + name_of_mic_play = "" + #lin_microphone_name = "ALC1150 Analog" + #lin_name_of_mic_play = "USB Sound Device" + +# These are for CM106 like sound device +#n2adr_sound_usb_mic = 'alsa:USB Sound Device' +#microphone_name = n2adr_sound_usb_mic +#mixer_settings = [ +# (microphone_name, 16, 1), # PCM capture from line +# (microphone_name, 14, 0), # PCM capture switch +# (microphone_name, 11, 1), # line capture switch +# (microphone_name, 12, 0.70), # line capture volume +# (microphone_name, 3, 0), # mic playback switch +# (microphone_name, 9, 0), # mic capture switch +# ] + +# These are for Asus internal sound +n2adr_sound_usb_mic = 'alsa:ALC1150 Analog' +microphone_name = n2adr_sound_usb_mic +mixer_settings = [ + (microphone_name, 19, 2), # PCM capture from line + (microphone_name, 24, 0), # PCM capture switch + (microphone_name, 22, 1), # line capture switch + (microphone_name, 27, 0), # line capture volume boost + (microphone_name, 21, 0.70), # line capture volume + ] diff --git a/n2adr/quisk_conf_8600.py b/n2adr/quisk_conf_8600.py new file mode 100644 index 0000000..8a5be31 --- /dev/null +++ b/n2adr/quisk_conf_8600.py @@ -0,0 +1,202 @@ +# These are the configuration parameters for receiving the +# 10.7 MHz IF output of the AOR AR8600 receiver with my +# transceiver. This results in a 100 kHz to 3 GHz +# wide range receiver with pan adapter. +# +# Due to noise starting at 11.18 MHz when tuned to 449.0 MHz, we tune to 10.5 MHz center. +# +# Note: The AR8600 IF output in WFM mode seems to tune in 10kHz increments +# no matter what the step size, even though the display reads a +# different frequency. + +# The AR8600 inverts the spectrum of these bands: 10, 2, 220, 440, 900 +# The AR8600 does not invert these bands: 1240 +# The change from inverted to non-inverted is about 1040 MHz. + +# Please do not change this sample file. +# Instead copy it to your own .quisk_conf.py and make changes there. +# See quisk_conf_defaults.py for more information. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import time, traceback, os +import _quisk as QS +import serial # From the pyserial package + +from n2adr.quisk_conf import * +from n2adr import scanner_widgets as quisk_widgets + +settings_file_path = "../quisk_settings.json" + +bandLabels = [ ('60',) * 5, '40', '20', + '15', '12', '10', '2', '220', '440', '900', '1240', ('Time',) * len(bandTime)] + +# Define the Hardware class in this config file instead of a separate file. + +from hiqsdr.quisk_hardware import Hardware as BaseHardware + +class Hardware(BaseHardware): + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.ar8600_frequency = 0 # current AR8600 tuning frequency + self.hware_frequency = 0 # current hardware VFO frequency + self.vfo_frequency = 0 # current Quisk VFO frequency + self.invert = 1 # The frequency spectrum is backwards + self.serial = None # the open serial port + self.timer = 0.02 # time between AR8600 commands in seconds + self.time0 = 0 # time of last AR8600 command + self.serial_out = [] # send commands slowly + self.offset = 10700000 # frequency offset from AR8600 tuning freq to IF output + self.tx_freq = 0 # current frequency + conf.BandEdge['220'] = (222000000, 225000000) + conf.BandEdge['440'] = (420000000, 450000000) + conf.BandEdge['900'] = (902000000, 928000000) + conf.BandEdge['1240'] = (1240000000, 1300000000) + rpt_file = os.path.normpath(os.path.join(os.getcwd(), '..')) + rpt_file = os.path.join(rpt_file, 'MetroCor.txt') + fp = open(rpt_file, 'r') + self.repeaters = {} + for line in fp: + line = line.strip() + if line and line[0] != '#': + line = line.split('\t') + fout = int(float(line[0]) * 1000000 + 0.1) + text = "%s %s, %s" % (line[2], line[3], line[5]) + if fout in self.repeaters: + self.repeaters[fout] = "%s ; %s" % (self.repeaters[fout], text) + else: + self.repeaters[fout] = text + fp.close() + rpt_file = os.path.normpath(os.path.join(os.getcwd(), '..')) + rpt_file = os.path.join(rpt_file, 'ARCC.csv') + fp = open(rpt_file, 'r') + for line in fp: + line = line.strip() + if line and line[0] != '#': + line = line.split(',') + fout = float(line[3]) + if fout >= 2000.0: + continue + fout = int(fout * 1000000 + 0.1) + text = "%s %s, %s" % (line[5], line[2], line[0]) + if fout in self.repeaters: + self.repeaters[fout] = "%s ; %s" % (self.repeaters[fout], text) + else: + self.repeaters[fout] = text + fp.close() + rpt_file = os.path.normpath(os.path.join(os.getcwd(), '..')) + rpt_file = os.path.join(rpt_file, 'Repeaters.csv') + fp = open(rpt_file, 'r') + for line in fp: + line = line.strip() + if line and line[0] != '#': + line = line.split(',') + fout = float(line[3]) + if fout >= 2000.0: + continue + fout = int(fout * 1000000 + 0.1) + if line[0]: + text = "%s %s, %s" % (line[5], line[2], line[0]) + else: + text = line[5] + if fout in self.repeaters: + self.repeaters[fout] = "%s ; %s" % (self.repeaters[fout], text) + else: + self.repeaters[fout] = text + fp.close() + for freq, text in list(self.repeaters.items()): + if len(text) > 80: + t ='' + stations = text.split(';') + for s in stations: + s = s.strip() + t = t + s.split()[0] + ' ' + s.split(',')[1] + '; ' + self.repeaters[freq] = t + self.rpt_freq_list = list(self.repeaters) + self.rpt_freq_list.sort() + def OpenPort(self): + if sys.platform == "win32": + tty_list = ("COM7", "COM8") + else: + tty_list = ("/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2") + for tty_name in tty_list: + try: + port = serial.Serial(port=tty_name, baudrate=9600, + stopbits=serial.STOPBITS_TWO, xonxoff=1, timeout=0) + except: + #traceback.print_exc() + pass + else: + time.sleep(0.1) + for i in range(3): + port.write('VR\r') + time.sleep(0.1) + chars = port.read(1024) + if "VR0101" in chars: + self.serial = port + port.write('MD0\r') # set WFM mode so the IF output is available + break + if self.serial: + break + else: + port.close() + def open(self): + self.OpenPort() + QS.invert_spectrum(self.invert) + t = BaseHardware.open(self) # save the message + return t + def close(self): + BaseHardware.close(self) + if self.serial: + self.serial.write('EX\r') + time.sleep(1) # wait for output to drain, but don't block + self.serial.close() + self.serial = None + def ChangeFrequency(self, tx_freq, vfo_freq, source='', band='', event=None): + self.tx_freq = tx_freq + try: + rpt = self.repeaters[tx_freq] + except KeyError: + self.application.bottom_widgets.UpdateText('') + else: + self.application.bottom_widgets.UpdateText(rpt) + if vfo_freq != self.vfo_frequency and vfo_freq >= 10000: + self.vfo_frequency = vfo_freq + # Calculate new AR8600 and hardware frequencies + ar8600 = (vfo_freq + 50000) // 100000 * 100000 - 200000 + if self.ar8600_frequency != ar8600: + self.ar8600_frequency = ar8600 + self.SendAR8600('RF%010d\r' % ar8600) + if ar8600 < 1040000000: + self.invert = 1 + else: + self.invert = 0 + QS.invert_spectrum(self.invert) + if self.invert: + hware = self.offset - vfo_freq + self.ar8600_frequency + else: + hware = self.offset + vfo_freq - self.ar8600_frequency + if self.hware_frequency != hware: + self.hware_frequency = hware + BaseHardware.ChangeFrequency(self, 0, hware) + #print 'AR8600 Hware', self.ar8600_frequency, self.hware_frequency + return tx_freq, vfo_freq + def SendAR8600(self, msg): # Send commands to the AR8600, but not too fast + if self.serial: + if time.time() - self.time0 > self.timer: + self.serial.write(msg) # send message now + self.time0 = time.time() + else: + self.serial_out.append(msg) # send message later + def HeartBeat(self): # Called at about 10 Hz by the main + BaseHardware.HeartBeat(self) + if self.serial: + chars = self.serial.read(1024) + #if chars: + # print chars + if self.serial_out and time.time() - self.time0 > self.timer: + self.serial.write(self.serial_out[0]) + self.time0 = time.time() + del self.serial_out[0] diff --git a/n2adr/quisk_hardware.py b/n2adr/quisk_hardware.py new file mode 100644 index 0000000..b4d8487 --- /dev/null +++ b/n2adr/quisk_hardware.py @@ -0,0 +1,80 @@ +# This is the hardware file from my shack, which controls various hardware. +# The files to control my 2010 transceiver and for the improved version HiQSDR +# are in the package directory HiQSDR. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from hiqsdr.quisk_hardware import Hardware as BaseHw +from n2adr import station_hardware + +class Hardware(BaseHw): + def __init__(self, app, conf): + BaseHw.__init__(self, app, conf) + self.GUI = None + self.vfo_frequency = 0 # current vfo frequency + self.rf_gain_labels = ('RF 0 dB', 'RF +16') + self.rf_gain = 0 # Preamp or attenuation in dB; changed via app.Hardware + # Other hardware + self.anttuner = station_hardware.AntennaTuner(app, conf) # Control the antenna tuner + #self.lpfilter = station_hardware.LowPassFilter(app, conf) # Control LP filter box + #self.hpfilter = station_hardware.HighPassFilter(app, conf) # Control HP filter box + self.controlbox = station_hardware.ControlBox(app, conf) # Control my Station Control Box + self.v2filter = station_hardware.FilterBoxV2(app, conf) # Control V2 filter box + def open(self): + if False: + from n2adr.station_hardware import StationControlGUI + self.GUI = StationControlGUI(self.application.main_frame, self, self.application, self.conf) + self.GUI.Show() + self.anttuner.open() + return BaseHw.open(self) + def close(self): + self.anttuner.close() + self.controlbox.close() + return BaseHw.close(self) + def ChangeFilterFrequency(self, tx_freq): + if tx_freq and tx_freq > 0: + if self.GUI: + self.GUI.SetTxFreq(tx_freq) + self.GUI.freq_entry.ChangeValue("%.3f" % (tx_freq * 1E-6)) + else: + self.anttuner.SetTxFreq(tx_freq) + self.v2filter.SetTxFreq(tx_freq) + def ChangeFrequency(self, tx_freq, vfo_freq, source='', band='', event=None): + self.ChangeFilterFrequency(tx_freq) + return BaseHw.ChangeFrequency(self, tx_freq, vfo_freq, source, band, event) + def ChangeBand(self, band): + # band is a string: "60", "40", "WWV", etc. + ret = BaseHw.ChangeBand(self, band) + self.anttuner.ChangeBand(band) + #self.lpfilter.ChangeBand(band) + #self.hpfilter.ChangeBand(band) + self.v2filter.ChangeBand(band) + self.CorrectSmeter() + return ret + def HeartBeat(self): # Called at about 10 Hz by the main + self.anttuner.HeartBeat() + #self.lpfilter.HeartBeat() + #self.hpfilter.HeartBeat() + self.v2filter.HeartBeat() + self.controlbox.HeartBeat() + return BaseHw.HeartBeat(self) + def OnSpot(self, level): + # level is -1 for Spot button Off; else the Spot level 0 to 1000. + self.anttuner.OnSpot(level) + return BaseHw.OnSpot(self, level) + def OnButtonRfGain(self, event): + #self.hpfilter.OnButtonRfGain(event) + self.v2filter.OnButtonRfGain(event) + self.CorrectSmeter() + def CorrectSmeter(self): # S-meter correction can change with band or RF gain + if self.band == '40': # Basic S-meter correction by band + self.correct_smeter = 20.5 + else: + self.correct_smeter = 20.5 + self.correct_smeter -= self.rf_gain / 6.0 # Correct S-meter for RF gain + self.application.waterfall.ChangeRfGain(self.rf_gain) # Waterfall colors are constant + def OnButtonPTT(self, event): + self.controlbox.OnButtonPTT(event) + return BaseHw.OnButtonPTT(self, event) diff --git a/n2adr/quisk_widgets.py b/n2adr/quisk_widgets.py new file mode 100644 index 0000000..1ed44fa --- /dev/null +++ b/n2adr/quisk_widgets.py @@ -0,0 +1,56 @@ +# Please do not change this widgets module for Quisk. Instead copy +# it to your own quisk_widgets.py and make changes there. +# +# This module is used to add extra widgets to the QUISK screen. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import wx, time +import _quisk as QS + +class BottomWidgets: # Add extra widgets to the bottom of the screen + def __init__(self, app, hardware, conf, frame, gbs, vertBox): + #self.config = conf + #self.hardware = hardware + #self.application = app + #start_row = app.widget_row # The first available row + #start_col = app.button_start_col # The start of the button columns + #b = app.QuiskCycleCheckbutton(frame, self.OnAntTuner, ('Antenna', 'Ant 0', 'Ant 1')) + #bw, bh = b.GetMinSize() + #gbs.Add(b, (start_row, start_col), (1, 2), flag=wx.EXPAND) + #b = app.QuiskPushbutton(frame, self.OnAntTuner, 'L+') + #b.Enable(0) + #gbs.Add(b, (start_row, start_col + 2), (1, 2), flag=wx.EXPAND) + #b = app.QuiskPushbutton(frame, self.OnAntTuner, 'L-') + #b.Enable(0) + #gbs.Add(b, (start_row, start_col + 4), (1, 2), flag=wx.EXPAND) + #b = app.QuiskPushbutton(frame, self.OnAntTuner, 'C+') + #b.Enable(0) + #gbs.Add(b, (start_row, start_col + 6), (1, 2), flag=wx.EXPAND) + #b = app.QuiskPushbutton(frame, self.OnAntTuner, 'C-') + #b.Enable(0) + #gbs.Add(b, (start_row, start_col + 8), (1, 2), flag=wx.EXPAND) + #b = app.QuiskPushbutton(frame, self.OnAntTuner, 'Save') + #b.Enable(0) + #gbs.Add(b, (start_row, start_col + 10), (1, 2), flag=wx.EXPAND) + #self.swr_label = app.QuiskText(frame, 'Watts 000 SWR 10.1 Zh Ind 22 Cap 33 Freq 28100 (7777)', bh) + #gbs.Add(self.swr_label, (start_row, start_col + 2), (1, 10), flag=wx.EXPAND) + #b = app.QuiskCheckbutton(frame, None, text='') + #gbs.Add(b, (start_row, start_col + 25), (1, 2), flag=wx.EXPAND) +# Example of a horizontal slider: +# lab = wx.StaticText(frame, -1, 'Preamp', style=wx.ALIGN_CENTER) +# gbs.Add(lab, (5,0), flag=wx.EXPAND) +# sl = wx.Slider(frame, -1, 1024, 0, 2048) # parent, -1, initial, min, max +# gbs.Add(sl, (5,1), (1, 5), flag=wx.EXPAND) +# sl.Bind(wx.EVT_SCROLL, self.OnPreamp) +# def OnPreamp(self, event): +# print event.GetPosition() + self.num_rows_added = 0 + #def OnAntTuner(self, event): + # btn = event.GetEventObject() + # text = btn.GetLabel() + # self.hardware.OnAntTuner(text) + #def UpdateText(self, text): + # self.swr_label.SetLabel(text) diff --git a/n2adr/scanner_widgets.py b/n2adr/scanner_widgets.py new file mode 100644 index 0000000..d2c86d7 --- /dev/null +++ b/n2adr/scanner_widgets.py @@ -0,0 +1,140 @@ +# Please do not change this widgets module for Quisk. Instead copy +# it to your own quisk_widgets.py and make changes there. +# +# This module is used to add extra widgets to the QUISK screen. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import wx, time +import _quisk as QS + +class BottomWidgets: # Add extra widgets to the bottom of the screen + def __init__(self, app, hardware, conf, frame, gbs, vertBox): + self.config = conf + self.hardware = hardware + self.application = app + row = 4 # The next available row + b = app.QuiskPushbutton(frame, None, 'Tune') + bw, bh = b.GetMinSize() + b.Enable(0) + gbs.Add(b, (row, 0), (1, 2), flag=wx.EXPAND) + b = app.QuiskPushbutton(frame, None, '') + gbs.Add(b, (row, 2), (1, 2), flag=wx.EXPAND) + b = app.QuiskPushbutton(frame, None, '') + gbs.Add(b, (row, 4), (1, 2), flag=wx.EXPAND) + b = self.btnScanner = app.QuiskCheckbutton(frame, self.OnBtnScanner, text='Scanner', use_right=True) + self.scan_timer = wx.Timer(b) # timed events for the scanner + b.Bind(wx.EVT_TIMER, self.OnTimerEvent) + gbs.Add(b, (row, 6), (1, 2), flag=wx.EXPAND) + b = self.btnNext = app.QuiskPushbutton(frame, self.OnBtnNext, 'Next', True) + gbs.Add(b, (row, 8), (1, 2), flag=wx.EXPAND) + b = app.QuiskCheckbutton(frame, self.OnBtnRptr, text='Rptr') + b.SetValue(True, True) + gbs.Add(b, (row, 10), (1, 2), flag=wx.EXPAND) + self.swr_label = app.QuiskText(frame, 'Watts 000 SWR 10.1 Zh Ind 22 Cap 33 Freq 28100 (7777)', bh) + gbs.Add(self.swr_label, (row, 15), (1, 12), flag=wx.EXPAND) +# Example of a horizontal slider: +# lab = wx.StaticText(frame, -1, 'Preamp', style=wx.ALIGN_CENTER) +# gbs.Add(lab, (5,0), flag=wx.EXPAND) +# sl = wx.Slider(frame, -1, 1024, 0, 2048) # parent, -1, initial, min, max +# gbs.Add(sl, (5,1), (1, 5), flag=wx.EXPAND) +# sl.Bind(wx.EVT_SCROLL, self.OnPreamp) +# def OnPreamp(self, event): +# print event.GetPosition() + def UpdateText(self, text): + self.swr_label.SetLabel(text) + def OnBtnRptr(self, event): + btn = event.GetEventObject() + if btn.GetValue(): + self.config.freq_spacing = 5000 + else: + self.config.freq_spacing = 0 + def OnBtnNext(self, event): + self.direction = self.btnNext.direction # +1 for left -> go up; -1 for down + self.keep_going = wx.GetKeyState(wx.WXK_SHIFT) # if Shift is down, move to next band + self.scanner = False + if self.keep_going: + if not self.ScanScreen(event): + self.MoveVfo(event) + self.scan_timer.Start(500) + else: + self.ScanScreen(event) + def ScanScreen(self, event): # Look for signals on the current screen + lst = self.hardware.rpt_freq_list + app = self.application + vfo = app.VFO + tx_freq = vfo + app.txFreq + sample_rate = app.sample_rate + limit = int(sample_rate / 2.0 * self.config.display_fraction * 0.95) # edge of screen + self.scan_n1 = None + self.scan_n = None + for n in range(len(lst)): + if lst[n] > vfo - limit and self.scan_n1 is None: + self.scan_n1 = n # inclusive + if lst[n] >= tx_freq and self.scan_n is None: + self.scan_n = n + if lst[n] > vfo + limit: + break + self.scan_n2 = n # inclusive + if self.scan_n is None: + self.scan_n = self.scan_n1 + if self.direction > 0: # left click; go up + seq = list(range(self.scan_n + 1, self.scan_n2 + 1)) + if not self.keep_going: + seq += list(range(self.scan_n1, self.scan_n)) + else: # right click; go down + seq = list(range(self.scan_n - 1, self.scan_n1 - 1, -1)) + if not self.keep_going: + seq += list(range(self.scan_n2, self.scan_n, -1)) + for n in seq: + freq = lst[n] + if not QS.get_squelch(freq - vfo): + app.ChangeHwFrequency(freq - vfo, vfo, 'Repeater', event) + return True # frequency was changed + return False # frequency was not changed + def MoveVfo(self, event): # Move the VFO to look for further signals + lst = self.hardware.rpt_freq_list + app = self.application + vfo = app.VFO + tx_freq = vfo + app.txFreq + sample_rate = app.sample_rate + if self.direction > 0: # left click; go up + n = self.scan_n2 + 1 + if n >= len(lst): + n = 0 + freq = lst[n] + vfo = freq + sample_rate * 4 // 10 + app.ChangeHwFrequency(freq - vfo, vfo, 'Repeater', event) + else: # right click; go down + n = self.scan_n1 - 1 + if n < 0: + n = len(lst) - 1 + freq = lst[n] + vfo = freq - sample_rate * 4 // 10 + app.ChangeHwFrequency(freq - vfo, vfo, 'Repeater', event) + def OnBtnScanner(self, event): + self.direction = self.btnScanner.direction # +1 for left -> go up; -1 for down + self.keep_going = wx.GetKeyState(wx.WXK_SHIFT) # if Shift is down, move to next band + self.scanner = True + if self.btnScanner.GetValue(): + self.btnNext.Enable(0) + if self.keep_going: + if not self.ScanScreen(event): + self.MoveVfo(event) + else: + self.ScanScreen(event) + self.scan_timer.Start(500) + else: + self.btnNext.Enable(1) + self.scan_timer.Stop() + def OnTimerEvent(self, event): + if QS.get_squelch(self.application.txFreq): + if self.keep_going: + if not self.ScanScreen(event): + self.MoveVfo(event) + else: + self.ScanScreen(event) + elif not self.scanner: + self.scan_timer.Stop() diff --git a/n2adr/startup.py b/n2adr/startup.py new file mode 100644 index 0000000..979077e --- /dev/null +++ b/n2adr/startup.py @@ -0,0 +1,92 @@ +#! /usr/bin/python + +# All QUISK software is Copyright (C) 2006-2011 by James C. Ahlstrom. +# This free software is licensed for use under the GNU General Public +# License (GPL), see http://www.opensource.org. +# Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!! + +"Select the desired hardware, and start Quisk" + +import sys, wx, subprocess, os + +Choices = [ +(' My Transceiver', 'n2adr/quisk_conf.py', ''), +(' VHF/UHF Receiver', 'n2adr/uhfrx_conf.py', ''), +(' Softrock Rx Ensemble', 'softrock/conf_rx_ensemble2.py', 'n2adr/conf2.py'), +(' Softrock Rx/Tx Ensemble', 'softrock/conf_rx_tx_ensemble.py', 'n2adr/conf6.py'), +(' Plain Sound Card, Rx only', 'n2adr/conf2.py', ''), +(' Test microphone sound', 'n2adr/conf4.py', ''), +(' SDR-IQ, receive only, antenna to RF input', 'quisk_conf_sdriq.py', 'n2adr/conf2.py'), +(' AOR AR8600 with IF to my hardware', 'n2adr/quisk_conf_8600.py', ''), +(' AOR AR8600 with IF to SDR-IQ', 'quisk_conf_sdr8600.py', 'n2adr/conf2.py'), +(' Fldigi with my transceiver', 'n2adr/quisk_conf.py', 'n2adr/conf5.py'), +(' Freedv.org Rx with my transceiver', 'n2adr/quisk_conf.py', 'n2adr/conf7.py'), +(' Hermes-Lite', 'hermes/quisk_conf.py', 'n2adr/conf3.py'), +(' Odyssey', 'odyssey/quisk_conf.py', 'n2adr/conf1.py'), +(' My Transceiver to Hermes-Lite', 'Quisk2Hermes', ''), +] + +if sys.platform == 'win32': + os.chdir('C:\\pub\\quisk') + exe = "C:\\python27\\pythonw.exe" +else: + os.chdir('/home/jim/pub/quisk') + exe = "/usr/bin/python" + +class ListBoxFrame(wx.Frame): + def __init__(self): + wx.Frame.__init__(self, None, -1, 'Select Hardware') + font = wx.Font(14, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL) + self.SetFont(font) + charx = self.GetCharWidth() + chary = self.GetCharHeight() + width = 0 + height = chary * 2 + tlist = [] + for txt, conf1, conf2 in Choices: + text = "%s, %s" % (txt, conf1) + if conf2: + text = "%s, %s" % (text, conf2) + tlist.append(text) + w, h = self.GetTextExtent(text) + width = max(width, w) + height += h + width += 3 * chary + lb = wx.ListBox(self, -1, (0, 0), (width, height), tlist, wx.LB_SINGLE) + lb.SetSelection(0) + lb.SetFont(font) + lb.Bind(wx.EVT_LISTBOX_DCLICK, self.OnDClick, lb) + lb.Bind(wx.EVT_KEY_DOWN, self.OnChar) + self.SetClientSize((width, height)) + def OnDClick(self, event): + lb = event.GetEventObject() + index = lb.GetSelection() + text, conf1, conf2 = Choices[index] + if conf1 == "Quisk2Hermes": + subprocess.Popen([exe, 'quisk.py', '-c', 'n2adr/quisk_conf.py', '--local', 'Q2H']) + subprocess.Popen([exe, 'quisk.py', '-c', 'hermes/quisk_conf.py', '--config2', 'n2adr/conf3A.py', '--local', 'Q2H']) + else: + cmd = [exe, 'quisk.py', '-c', conf1] + if conf2: + cmd = cmd + ['--config2', conf2] + subprocess.Popen(cmd) + self.Destroy() + def OnChar(self, event): + if event.GetKeyCode() == 13: + self.OnDClick(event) + else: + event.Skip() + +class App(wx.App): + def __init__(self): + if sys.stdout.isatty(): + wx.App.__init__(self, redirect=False) + else: + wx.App.__init__(self, redirect=True) + def OnInit(self): + frame = ListBoxFrame() + frame.Show() + return True + +app = App() +app.MainLoop() diff --git a/n2adr/station_hardware.py b/n2adr/station_hardware.py new file mode 100644 index 0000000..662f689 --- /dev/null +++ b/n2adr/station_hardware.py @@ -0,0 +1,1092 @@ +# This file supports various hardware boxes at my shack + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import sys, struct, math, socket, select, time, traceback, os +import wx, wx.lib.buttons + +DEBUG = 0 + +gatewayTime = 0 # time of last gateway command +gatewayLimit = 0.2 # minimum time between gateway commands + +class ControlBox: # Control my station control box + address = ('192.168.1.194', 0x3A00 + 64) + def __init__(self, app, conf): + self.application = app # Application instance (to provide attributes) + self.conf = conf # Config file module + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.setblocking(0) + self.socket.connect(self.address) + self.have_data = b'C\000' + self.want_data = b'C\000' + self.timer = 0 + def close(self): + self.want_data = b'C\000' # raise key if down + if self.have_data != self.want_data: + self.socket.send(self.want_data) + time.sleep(0.1) + self.socket.send(self.want_data) + def OnButtonPTT(self, event): + btn = event.GetEventObject() + if btn.GetValue(): # Turn the software key bit on or off + self.want_data = b'C\001' + else: + self.want_data = b'C\000' + def SetKeyDown(self, down): + if down: # Turn the software key bit on or off + self.want_data = b'C\001' + else: + self.want_data = b'C\000' + def HeartBeat(self): + global gatewayTime + if not self.socket: + return + try: # The control box echoes its commands + self.have_data = self.socket.recv(50) + except socket.error: + pass + except socket.timeout: + pass + if self.have_data != self.want_data and time.time() - gatewayTime > gatewayLimit: + gatewayTime = time.time() + if self.timer <= 10: + self.timer += 1 + if self.timer == 10: + print ('Control box error') + try: + self.socket.send(self.want_data) + except socket.error: + pass + except socket.timeout: + pass + +class LowPassFilter: # Control my low pass filter box + address = ('192.168.1.194', 0x3A00 + 39) + # Filters are numbered 1 thru 8 for bands: 80, 15, 60, 40, 30, 20, 17, short + lpfnum = (1, 1, 1, 1, 1, 3, # frequency 0 thru 5 MHz + 4, 4, 5, 5, 5, # 6 thru 10 + 6, 6, 6, 6, 7, # 11 thru 15 + 7, 7, 7, 2, 2, # 16 thru 20 + 2, 2, 8, 8, 8) # 21 thru 25; otherwise the filter is 8 + def __init__(self, app, conf): + self.application = app # Application instance (to provide attributes) + self.conf = conf # Config file module + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.setblocking(0) + self.socket.connect(self.address) + self.have_data = None + self.want_data = b'\000' + self.old_tx_freq = 0 + self.timer = 0 + def ChangeBand(self, band): + pass + def SetTxFreq(self, tx_freq): + if not self.socket: + return + # Filters are numbered 1 thru 8 + if abs(self.old_tx_freq - tx_freq) < 100000: + return # Ignore small tuning changes + self.old_tx_freq = tx_freq + try: # Look up filter number based on MHz + num = self.lpfnum[tx_freq // 1000000] + except IndexError: + num = 8 + self.want_data = bytearray((num, )) + self.timer = 0 + #print ("LP filter band %d" % num) + def HeartBeat(self): + global gatewayTime + if not self.socket: + return + try: # The HP filter box echoes its commands + self.have_data = self.socket.recv(50) + except socket.error: + pass + except socket.timeout: + pass + if self.have_data != self.want_data and time.time() - gatewayTime > gatewayLimit: + gatewayTime = time.time() + if self.timer <= 10: + self.timer += 1 + if self.timer == 10: + print ('Low pass filter error') + try: + self.socket.send(self.want_data) + except socket.error: + pass + except socket.timeout: + pass + +class HighPassFilter: # Control my high pass filter box + address = ('192.168.1.194', 0x3A00 + 21) + def __init__(self, app, conf): + self.application = app # Application instance (to provide attributes) + self.conf = conf # Config file module + self.preamp = 0 + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.setblocking(0) + self.socket.connect(self.address) + self.have_data = None + self.want_data = bytes(3) + self.old_tx_freq = 0 + self.timer = 0 + def ChangeBand(self, band): + btn = self.application.BtnRfGain + freq = self.application.VFO + self.application.txFreq + if self.conf.use_sdriq: + if btn: + if freq < 5000000: + btn.SetLabel('RF -10', True) + elif freq < 13000000: + btn.SetLabel('RF 0 dB', True) + else: + btn.SetLabel('RF +16', True) + elif self.conf.use_rx_udp: + if btn: + if freq < 5000000: + btn.SetLabel('RF 0 dB', True) + elif freq < 13000000: + btn.SetLabel('RF 0 dB', True) + else: + btn.SetLabel('RF +16', True) + def OnButtonRfGain(self, event): + """Set my High Pass Filter Box preamp gain and attenuator state.""" + btn = event.GetEventObject() + n = btn.index + if n == 0: # 0dB + self.preamp = 0x00 + self.application.Hardware.rf_gain = 0 + elif n == 1: # +16 + self.preamp = 0x02 + self.application.Hardware.rf_gain = 16 + elif n == 2: # -20 + self.preamp = 0x0C + self.application.Hardware.rf_gain = -20 + elif n == 3: # -10 + self.preamp = 0x04 + self.application.Hardware.rf_gain = -10 + else: + print ('Unknown RfGain') + self.application.Hardware.rf_gain = 0 + self.SetTxFreq(None) + def SetTxFreq(self, tx_freq): + """Set high pass filter and preamp/attenuator state""" + # Filter cutoff in MHz: 0.0, 2.7, 3.95, 5.7, 12.6, 18.2, 22.4 + # Frequency MHz Bits Hex Band + # ============= ==== === ==== + # 0 to 2.70 PORTD, 0 0x01 160 + # 2.7 to 3.95 PORTB, 1 0x02 80 + # 3.95 to 5.70 PORTD, 7 0x80 60 + # 5.70 to 12.60 PORTB, 0 0x01 40, 30 + # 12.60 to 18.20 PORTD, 6 0x40 20, 17 + # 18.20 to 22.40 PORTB, 7 0x80 15 + # 22.40 to 99.99 PORTB, 6 0x40 12, 10 + # Other bits: Preamp PORTD 0x02, Atten1 PORTD 0x04, Atten2 PORTD 0x08 + if not self.socket: + return + if tx_freq is None: + tx_freq = self.old_tx_freq + elif abs(self.old_tx_freq - tx_freq) < 100000: + return # Ignore small tuning changes + self.old_tx_freq = tx_freq + portb = portc = portd = 0 + if self.conf.use_sdriq: + if tx_freq < 15000000: # Turn preamp on/off + self.preamp = 0x00 + else: + self.preamp = 0x02 + elif self.conf.use_rx_udp: + pass # self.preamp is already set + else: # turn preamp off + self.preamp = 0x00 + if tx_freq < 12600000: + if tx_freq < 3950000: + if tx_freq < 2700000: + portd = 0x01 + else: + portb = 0x02 + elif tx_freq < 5700000: + portd = 0x80 + else: + portb = 0x01 + elif tx_freq < 18200000: + portd = 0x40 + elif tx_freq < 22400000: + portb = 0x80 + else: + portb = 0x40 + portd |= self.preamp + self.want_data = bytearray((portb, portc, portd)) + self.timer = 0 + def HeartBeat(self): + global gatewayTime + if not self.socket: + return + try: # The HP filter box echoes its commands + self.have_data = self.socket.recv(50) + except socket.error: + pass + except socket.timeout: + pass + if self.have_data != self.want_data and time.time() - gatewayTime > gatewayLimit: + gatewayTime = time.time() + if self.timer <= 10: + self.timer += 1 + if self.timer == 10: + print ('High pass filter error') + try: + self.socket.send(self.want_data) + except socket.error: + pass + except socket.timeout: + pass + +class FilterBoxV2: # Control my 2016 high/low pass filter box + address = ('192.168.1.194', 0x3A00 + 70) + # Low pass filters are numbered 0 thru 5 for bands: 10, 15, 17, 20, 40, 60 + lpfnum = (5, 5, 5, 5, 5, 5, # frequency 0 thru 5 MHz + 4, 4, 3, 3, 3, # 6 thru 10 + 3, 3, 3, 3, 2, # 11 thru 15 + 2, 2, 2, 1, 1, # 16 thru 20 + 1, 0, 0, 0, 0) # 21 thru 25; otherwise the filter is 0 + # High pass filters are numbered 0 thru 2 with cutoff 12.5 MHz, 4.2 MHz, short + hpfnum = (2, 2, 2, 2, 2, 1, # frequency 0 thru 5 MHz + 1, 1, 1, 1, 1, # 6 thru 10 + 1, 1, 0, 0, 0, # 11 thru 15 + 0, 0, 0, 0, 0, # 16 thru 20 + 0, 0, 0, 0, 0) # 21 thru 25; otherwise the filter is 0 + def __init__(self, app, conf): + self.application = app # Application instance (to provide attributes) + self.conf = conf # Config file module + self.preamp = 0 + self.have_data = None + self.want_data = b'\x00\x00' + self.old_tx_freq = 0 + self.timer = 0 + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.setblocking(0) + self.socket.connect(self.address) + def ChangeBand(self, band): + pass + def DefaultPreamp(self, freq): + preamp = 0 + if self.conf.use_rx_udp: + btn = self.application.BtnRfGain + if btn: + if freq < 5000000: + btn.SetLabel('RF 0 dB', False) + elif freq < 13000000: + btn.SetLabel('RF 0 dB', False) + else: + btn.SetLabel('RF +16', False) + preamp = 1 + return preamp + def OnButtonRfGain(self, event): + btn = event.GetEventObject() + n = btn.index + if n == 0: # 0dB + self.preamp = 0 + self.application.Hardware.rf_gain = 0 + elif n == 1: # +16 + self.preamp = 1 + self.application.Hardware.rf_gain = 16 + else: + print ('Unknown RfGain') + self.preamp = 0 + self.application.Hardware.rf_gain = 0 + if DEBUG: print ('Gain', self.preamp, self.application.Hardware.rf_gain) + self.SetTxFreq(None) + def SetPreamp(self, preamp): + if preamp: + self.preamp = 1 + else: + self.preamp = 0 + if DEBUG: print ("Preamp", preamp) + self.SetTxFreq(None) + def SetTxFreq(self, tx_freq): + if tx_freq is None: + tx_freq = self.old_tx_freq + elif abs(self.old_tx_freq - tx_freq) < 100000: + return # Ignore small tuning changes + if abs(self.old_tx_freq - tx_freq) > 1000000: + self.preamp = self.DefaultPreamp(tx_freq) + self.old_tx_freq = tx_freq + try: # Look up low pass filter number based on MHz + lpf = self.lpfnum[tx_freq // 1000000] + except IndexError: + lpf = 0 + try: # Look up high pass filter number based on MHz + hpf = self.hpfnum[tx_freq // 1000000] + except IndexError: + hpf = 0 + if DEBUG: print ("V2 filter LPF %d HPF %d" % (lpf, hpf)) + lpf = 1 << lpf + hpf = 1 << hpf + if self.preamp: + hpf |= 0b10000000 + self.want_data = bytearray((lpf, hpf)) + self.timer = 0 + def HeartBeat(self): + global gatewayTime + if not self.socket: + return + try: # The V2 filter box echoes its commands + self.have_data = self.socket.recv(50) + except socket.error: + pass + except socket.timeout: + pass + if self.have_data != self.want_data and time.time() - gatewayTime > gatewayLimit: + gatewayTime = time.time() + if self.timer <= 10: + self.timer += 1 + if self.timer == 10: + print ('V2 filter box error') + try: + self.socket.send(self.want_data) + #print ('V2 filter box send data 0x%X 0x%X' % (ord(self.want_data[0]), ord(self.want_data[1]))) + except socket.error: + pass + except socket.timeout: + pass + +class AntennaControl: # Control my KI8BV dipole + AntCtrlAddress = ('192.168.1.194', 0x3A00 + 33) + def __init__(self, app, conf): + self.application = app # Application instance (to provide attributes) + self.conf = conf # Config file module + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.setblocking(0) + self.socket.connect(self.AntCtrlAddress) + self.have_data = None + self.want_data = b'\00' + self.timer = 0 + def SetTxFreq(self, tx_freq): + self.timer = 0 + if tx_freq < 19000000: + self.want_data = b'\03' + elif tx_freq < 22000000: + self.want_data = b'\02' + elif tx_freq < 26500000: + self.want_data = b'\01' + else: + self.want_data = b'\00' + def HeartBeat(self): + global gatewayTime + try: # The antenna control box echoes its commands + self.have_data = self.socket.recv(50) + except socket.error: + pass + except socket.timeout: + pass + if self.have_data != self.want_data and time.time() - gatewayTime > gatewayLimit: + gatewayTime = time.time() + self.timer += 1 + if self.timer == 10: + print ('Antenna control error') + self.timer = 0 + try: + self.socket.send(self.want_data) + #print ("Change dipole to ord %d" % ord(self.want_data)) + except socket.error: + pass + except socket.timeout: + pass + +class AntennaTuner: # Control my homebrew antenna tuner and my KI8BV dipole + address = ('192.168.1.194', 0x3A00 + 47) + def __init__(self, app, conf): + self.application = app # Application instance (to provide attributes) + self.conf = conf # Config file module + self.socket = None + self.have_data = b'\xFF\xFF\xFF' + self.tx_freq = 0 + self.tune_freq = 0 # Frequency we last tuned + self.timer = 0 + self.set_L = 0 + self.set_C = 0 + self.set_HighZ = 0 + self.antnum = 0 # Antenna number 0 or 1 + self.dipole2 = AntennaControl(app, conf) # Control the KI8BV dipole + if False and conf.use_rx_udp == 10: # Hermes UDP protocol for Hermes-Lite2 + path = 'TunerLCZ_HL2.txt' + else: + path = 'TunerLCZ.txt' + if sys.platform == "win32": + path = 'C:/pub/' + path + else: + path = '/home/jim/pub/' + path + fp = open(path, "r") + lines = fp.readlines() + fp.close() + self.TunerLCZ = [(0, 0, 0, 0), (999111000, 0, 0, 0)] # Add dummy first and last entry. + for line in lines: + freq, antL, antC, hilo = line.split() + freq = int(freq) + antL = int(antL) + antC = int(antC) + hilo = int(hilo) + self.TunerLCZ.append((freq, antL, antC, hilo)) + self.TunerLCZ.sort() + self.WantData() + def WantData(self): + if self.set_HighZ: + flags = 0x00 + else: + flags = 0x08 + if self.antnum: + flags |= 0x04 + self.want_data = bytearray((self.set_C, self.set_L, flags)) + def open(self): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.setblocking(0) + self.socket.connect(self.address) + def close(self): + pass + def OnSpot(self, level): + # level is -1 for Spot button Off; else the Spot level 0 to 1000. + pass + def ChangeBand(self, band): + pass + def FindLCZ(self, tx_freq): + i1 = 0 + i2 = len(self.TunerLCZ) - 1 + while 1: # binary partition + i = (i1 + i2) // 2 + if self.TunerLCZ[i][0] < tx_freq: + i1 = i + else: + i2 = i + if i2 - i1 <= 1: + break + # The correct setting is between i1 and i2. + # Choose the index that is closest in frequency. + delta1 = tx_freq - self.TunerLCZ[i1][0] + delta2 = self.TunerLCZ[i2][0] - tx_freq + if i1 == 0: # below the first frequency + index = i1 + elif i2 == len(self.TunerLCZ) - 1: # above the last frequency + index = i2 + elif delta2 < delta1: # i2 is closer + index = i2 + else: # i1 is closer + index = i1 + if DEBUG: print ("AntennaTuner FindLCZ", tx_freq, i1, i2, len(self.TunerLCZ), self.TunerLCZ[index]) + return self.TunerLCZ[index] + def SetTxFreq(self, tx_freq, no_tune=False): + #if DEBUG: print("AntennaTuner SetTxFreq", tx_freq) + self.tx_freq = tx_freq + if tx_freq is None: + return + if tx_freq < 17000000: + if DEBUG and self.antnum == 1: print ("antnum 0") + self.antnum = 0 + else: + if DEBUG and self.antnum == 0: print ("antnum 1") + self.antnum = 1 + if self.antnum == 1: + self.dipole2.SetTxFreq(tx_freq) + if not self.socket: + return + if no_tune: # Change dipole but don't tune the antenna tuner + return + if abs(self.tune_freq - tx_freq) < 5000: # ignore small changes + return + self.tune_freq = tx_freq + f, newL, newC, newH = self.FindLCZ(tx_freq) + if newH != self.set_HighZ or self.set_C != newC or self.set_L != newL: + self.set_HighZ = newH + self.set_C = newC + self.set_L = newL + self.WantData() + def HeartBeat(self): + global gatewayTime + if not self.socket: + return + try: # The tuner echoes its command + self.have_data = data = self.socket.recv(50) + except socket.error: + data = '' + except socket.timeout: + data = '' + if self.have_data != self.want_data and time.time() - gatewayTime > gatewayLimit: + gatewayTime = time.time() + self.Send() + self.timer += 1 + if self.timer == 10: + print ('Antenna tuner error') + self.timer = 0 + else: + self.timer = 0 + self.dipole2.HeartBeat() + def Send(self): + try: + self.socket.send(self.want_data) + except socket.error: + pass + except socket.timeout: + pass + +class StationControlGUI(wx.Frame): # Display a stand-alone control window for my antenna tuner + def __init__(self, frame, hware, app, conf): + wx.Frame.__init__(self, parent=frame, title="Station Control", + style=wx.CAPTION|wx.CLOSE_BOX) + self.hware = hware + self.application = app + self.conf = conf + self.data_saving = False + self.tx_freq = 0 + if sys.platform == "win32": + self.filename = 'C:/pub/TunerLCZ.tmp' + else: + self.filename = '/home/jim/pub/TunerLCZ.tmp' + try: + fp = open(self.filename, "r") + except FileNotFoundError: + lines = () + else: + lines = fp.readlines() + fp.close() + self.TunerLCZ = [(0, 0, 0, 0), (999111000, 0, 0, 0)] # Add dummy first and last entry. + for line in lines: + freq, antL, antC, hilo = line.split() + freq = int(freq) + antL = int(antL) + antC = int(antC) + hilo = int(hilo) + self.TunerLCZ.append((freq, antL, antC, hilo)) + self.TunerLCZ.sort() + self.SetBackgroundColour('light steel blue') + self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyDown) + sizer = wx.GridBagSizer(hgap=5, vgap=3) + font = wx.Font(12, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + row = 0 + szr = wx.BoxSizer(wx.HORIZONTAL) + self.bands = [] + for band in ('160', '80', '60', '40', '30', '20', '17', '15', '12', '10'): + b = wx.lib.buttons.GenToggleButton(self, -1, band) + b.SetUseFocusIndicator(False) + b.SetBezelWidth(4) + self.bands.append(b) + szr.Add(b) + self.Bind(wx.EVT_BUTTON, self.OnBtnBand, b) + sizer.Add(szr, pos=(row, 0), span=(1, 3), flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT|wx.RIGHT|wx.TOP, border=20) + row = 1 + b = wx.StaticText(self, -1, " Tuner Adjust L ", style=wx.ALIGN_CENTER) + b.SetFont(font) + sizer.Add(b, pos=(row, 0), flag=wx.ALIGN_CENTER_VERTICAL) + b = self.sliderL = wx.Slider(self, -1, 0, 0, 255, size=(700, -1), style=wx.SL_HORIZONTAL|wx.SL_LABELS) + b.Bind(wx.EVT_SCROLL, self.OnSliderL) + sizer.Add(b, pos=(row,1)) + b = self.btnHiLo = wx.lib.buttons.GenToggleButton(self, -1, "HiLo") + b.SetUseFocusIndicator(False) + b.SetBezelWidth(4) + b.SetFont(font) + self.Bind(wx.EVT_BUTTON, self.OnButtonHiLo, b) + sizer.Add(b, pos=(row,2), flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT|wx.RIGHT, border=20) + row = 2 + b = wx.StaticText(self, -1, " Tuner Adjust C ") + b.SetFont(font) + sizer.Add(b, pos=(row, 0), flag=wx.ALIGN_CENTER_VERTICAL) + b = self.sliderC = wx.Slider(self, -1, 0, 0, 255, size=(700, -1), style=wx.SL_HORIZONTAL|wx.SL_LABELS) + b.Bind(wx.EVT_SCROLL, self.OnSliderC) + sizer.Add(b, pos=(row,1), flag=wx.ALIGN_CENTER_VERTICAL) + b = wx.lib.buttons.GenButton(self, -1, "Save") + b.SetUseFocusIndicator(False) + b.SetBezelWidth(4) + b.SetFont(font) + self.Bind(wx.EVT_BUTTON, self.OnButtonSave, b) + sizer.Add(b, pos=(row,2), flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT|wx.RIGHT, border=20) + row = 3 + szr = wx.BoxSizer(wx.HORIZONTAL) + szr.AddSpacer(20) + b = wx.lib.buttons.GenToggleButton(self, -1, "Key Down") + b.SetFont(font) + b.SetUseFocusIndicator(False) + b.SetBezelWidth(4) + self.Bind(wx.EVT_BUTTON, self.OnBtnKey, b) + szr.Add(b) + szr.AddSpacer(10) + b = wx.lib.buttons.GenToggleButton(self, -1, "FilterV2 Preamp") + b.SetFont(font) + b.SetUseFocusIndicator(False) + b.SetBezelWidth(4) + if conf.use_rx_udp == 10: # Hermes UDP protocol for Hermes-Lite2 + b.Enable(False) + self.Bind(wx.EVT_BUTTON, self.OnBtnV2Preamp, b) + szr.Add(b) + b = wx.StaticText(self, -1, " Freq MHz ") + b.SetFont(font) + szr.Add(b, flag=wx.ALIGN_CENTER_VERTICAL) + szr.AddSpacer(10) + self.freq_entry = wx.TextCtrl(self, value='0000.000', style=wx.TE_PROCESS_ENTER) + self.freq_entry.SetFont(font) + self.Bind(wx.EVT_TEXT_ENTER, self.OnFreqEntry, self.freq_entry) + szr.Add(self.freq_entry) + szr.AddSpacer(10) + b = wx.lib.buttons.GenButton(self, -1, "Search Freq") + b.SetUseFocusIndicator(False) + b.SetBezelWidth(4) + b.SetFont(font) + self.Bind(wx.EVT_BUTTON, self.OnButtonSearch, b) + szr.Add(b) + sizer.Add(szr, pos=(row, 0), span=(1, 3)) + if frame is None: + self.SetSizerAndFit(sizer) + else: + self.SetSizer(sizer) + self.Fit() + w, h = self.GetSize().Get() + self.SetSize((w + 20, h + 50)) + self.Bind(wx.EVT_CLOSE, self.OnBtnClose) + if frame is None: # Start a HeartBeat if the frame does not provide one + self.timer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.HeartBeat) + self.timer.Start(100) + def SetTxFreq(self, tx_freq): + #print("StationControlGUI SetTxFreq", tx_freq) + self.tx_freq = tx_freq + index, tup = self.FindIndexTup(tx_freq) + self.sliderL.SetValue(tup[1]) + self.sliderC.SetValue(tup[2]) + self.btnHiLo.SetValue(tup[3]) + anttuner = self.hware.anttuner + fff, anttuner.set_L, anttuner.set_C, anttuner.set_HighZ = tup + if tx_freq < 17000000: + anttuner.antnum = 0 + else: + anttuner.antnum = 1 + if anttuner.antnum == 1: + anttuner.dipole2.SetTxFreq(tx_freq) + anttuner.WantData() + def FindIndexTup(self, tx_freq): + i1 = 0 + i2 = len(self.TunerLCZ) - 1 + while 1: # binary partition + i = (i1 + i2) // 2 + if self.TunerLCZ[i][0] < tx_freq: + i1 = i + else: + i2 = i + if i2 - i1 <= 1: + break + # The correct setting is between i1 and i2. + # Choose the index that is closest in frequency. + delta1 = tx_freq - self.TunerLCZ[i1][0] + delta2 = self.TunerLCZ[i2][0] - tx_freq + if i1 == 0: # below the first frequency + index = i1 + elif i2 == len(self.TunerLCZ) - 1: # above the last frequency + index = i2 + elif delta2 < delta1: # i2 is closer + index = i2 + else: # i1 is closer + index = i1 + if DEBUG: print ("StatuinControlGUI FindLCZ", tx_freq, i1, i2, len(self.TunerLCZ), index, self.TunerLCZ[index]) + return index, self.TunerLCZ[index] + def OnBtnClose(self, event): + self.hware.anttuner.close() + self.hware.controlbox.close() + self.Destroy() + def HeartBeat(self, event): + self.hware.anttuner.HeartBeat() + self.hware.controlbox.HeartBeat() + if self.hware.v2filter: + self.hware.v2filter.HeartBeat() + def Open(self): # Initialize the hardware + self.bands[0].SetToggle(True) # 160 meters + self.hware.anttuner.open() + if self.hware.v2filter: + self.hware.v2filter.SetTxFreq(1900000) # Start 160 meters + def OnKeyDown(self, event): + key = event.GetRawKeyCode() + if key == 76: # L + self.sliderL.SetValue(self.sliderL.GetValue() + 1) + self.OnSliderL() + elif key == 108: # l + self.sliderL.SetValue(self.sliderL.GetValue() - 1) + self.OnSliderL() + elif key == 67: # C + self.sliderC.SetValue(self.sliderC.GetValue() + 1) + self.OnSliderC() + elif key == 99: # c + self.sliderC.SetValue(self.sliderC.GetValue() - 1) + self.OnSliderC() + else: + event.Skip() + def OnBtnBand(self, event): + btn = event.GetEventObject() + for band in self.bands: + if band == btn: + band.SetToggle(True) + else: + band.SetToggle(False) + band = btn.GetLabel() + f1, f2 = self.conf.BandEdge[band] + f = (f1 + f2) // 2 + f = ((f + 500) // 1000) * 1000 + if self.hware.v2filter: + self.hware.v2filter.SetTxFreq(f) + self.hware.anttuner.SetTxFreq(f, no_tune=True) + self.freq_entry.ChangeValue("%.3f" % (f * 1E-6)) + self.SetTxFreq(f) + def OnButtonHiLo(self, event): + anttuner = self.hware.anttuner + if self.btnHiLo.GetValue(): + anttuner.set_HighZ = 1 + else: + anttuner.set_HighZ = 0 + anttuner.WantData() + def OnFreqEntry(self, event=None): + freq = self.freq_entry.GetValue() + freq = float(freq) * 1E6 + freq = int(freq + 0.1) + self.tx_freq = freq + def OnButtonSearch(self, event): + freq = self.freq_entry.GetValue() + freq = float(freq) * 1E6 + freq = int(freq + 0.1) + self.tx_freq = freq + self.SetTxFreq(self.tx_freq) + def OnButtonSave(self, event): + freq = self.freq_entry.GetValue() + freq = float(freq) * 1E6 + freq = int(freq + 0.1) + self.tx_freq = freq + L = self.sliderL.GetValue() + C = self.sliderC.GetValue() + if self.btnHiLo.GetValue(): + hilo = 1 + else: + hilo = 0 + tup = (self.tx_freq, L, C, hilo) + index, t = self.FindIndexTup(self.tx_freq) + if self.tx_freq == self.TunerLCZ[index][0]: + self.TunerLCZ[index] = tup + else: + self.TunerLCZ.append(tup) + self.TunerLCZ.sort() + fp = open(self.filename, 'w') + for tup in self.TunerLCZ[1:-1]: # Throw away dummy entries + fp.write("%12d %2d %2d %d\n" % tup) + fp.close() + def OnSliderL(self, event=None): + self.hware.anttuner.set_L = self.sliderL.GetValue() + self.hware.anttuner.WantData() + def OnSliderC(self, event=None): + self.hware.anttuner.set_C = self.sliderC.GetValue() + self.hware.anttuner.WantData() + def OnBtnV2Preamp(self, event): + if self.hware.v2filter: + self.hware.v2filter.SetPreamp(event.GetEventObject().GetValue()) + def OnBtnKey(self, event): + self.hware.controlbox.SetKeyDown(event.GetEventObject().GetValue()) + +class AT200PC: # Control an AT-200PC autotuner made by LDG + def __init__(self, app, conf): + import serial + self.application = app # Application instance (to provide attributes) + self.conf = conf # Config file module + self.serial = None + self.rx_state = 0 + self.is_standby = None + self.tx_freq = 0 + self.old_tx_freq = 0 + self.set_L = -9 + self.set_C = -9 + self.set_HiLoZ = -9 + self.tuning_F1 = 0 + self.tuning_F2 = 0 + self.tuning_diff = 0 + self.param1 = [None] * 20 # Parameters returned by the AT-200PC + self.param2 = [None] * 20 + self.param1[5] = self.param2[5] = self.param2[6] = 0 # power and SWR + self.param1[7] = self.param2[7] = 1 # Frequency + self.param1[1] = self.param1[2] = 0 # Inductor, Capacitor + self.req_swr = 50 # Requested SWR: 50 thru 56 for 1.1, 1.3, 1.5, 1.7, 2.0, 2.5, 3.0 + self.live_update = 0 # Request live update 1 or 0 + self.antenna = 2 # Select antenna 1 or 2 + self.standby = 0 # Set standby mode 1 or 0 + self.timer = 0 + self.error = "AT-200PC serial port is not open" + self.TunerLC_change = False + if sys.platform == "win32": + self.TunerLC_fname = 'C:/pub/TunerLC.txt' + else: + self.TunerLC_fname = '/home/jim/pub/TunerLC.txt' + def UpdateSwr(self): + if not self.application.bottom_widgets: + return + if self.error: + self.application.bottom_widgets.UpdateText(self.error) + else: + power = (self.param1[5] * 256 + self.param2[5]) / 100.0 + swr = self.param2[6] # swr code = 256 * p**2 + if power >= 2.0: + freq = self.param1[7] * 256 + self.param2[7] + freq = 20480000.0 / freq + ftext = "Tx freq" + swr = math.sqrt(swr / 256.0) + swr = (1.0 + swr) / (1.0 - swr) + if swr > 99.9: + swr = 99.9 + else: + freq = self.tuning_diff / 1000.0 + ftext = "Tune delta" + swr = 0.0 + if self.param1[3] == 0: # HiLoZ relay value + t = "Zh" # High + else: + t = "Zl" # Low + text = "Watts %.0f SWR %.1f %s Ind %d Cap %d %s %.0f kHz" % ( + power, swr, t, self.param1[1], self.param1[2], ftext, freq) + self.application.bottom_widgets.UpdateText(text) + def HeartBeat(self): + if not self.serial: + self.UpdateSwr() + return + self.Read() # Receive from the AT-200PC + # Call main application with new SWR data + self.UpdateSwr() + if self.error: # Send a couple parameters, see if we get a response + if self.req_swr - 50 != self.param1[16]: + self.Write(chr(self.req_swr)) # Send threshold SWR + elif self.param1[17] != 0: + self.Write(chr(59)) # Turn off AutoTune + else: + self.error = '' + return + if self.param1[4] != self.antenna - 1: # Check correct antenna + self.Write(chr(9 + self.antenna)) + elif self.is_standby != self.standby: # Check standby state + self.Write(chr(45 - self.standby)) + elif self.param1[19] != self.live_update: # Check live update state + self.Write(chr(64 - self.live_update)) + elif self.set_L >= 0 and self.set_HiLoZ >= 0 and ( # Check L and Hi/Lo relay + self.param1[1] != self.set_L or self.param1[3] != self.set_HiLoZ): + if self.set_HiLoZ: + self.Write(chr(65) + chr(self.set_L + 128)) + else: + self.Write(chr(65) + chr(self.set_L)) + elif self.param1[2] != self.set_C and self.set_C >= 0: # Set C + self.Write(chr(66) + chr(self.set_C)) + elif self.live_update: # If our window shows, request an update + self.timer += 1 + if self.timer > 20: + self.timer = 0 + self.Write(chr(40)) # Request ALLUPDATE + def Write(self, s): # Write a command string to the AT-200PC + if DEBUG: + print ('Send', ord(s[0])) + if self.serial: + self.serial.setRTS(1) # Wake up the AT-200PC + time.sleep(0.003) # Wait 3 milliseconds + self.serial.write(s) + self.serial.setRTS(0) + def Read(self): # Receive characters from the AT-200PC + chars = self.serial.read(1024) + for ch in chars: + if self.rx_state == 0: # Read first of 4 characters; must be decimal 165 + if ord(ch) == 165: + self.rx_state = 1 + elif self.rx_state == 1: # Read second byte + self.rx_state = 2 + self.rx_byte1 = ord(ch) + elif self.rx_state == 2: # Read third byte + self.rx_state = 3 + self.rx_byte2 = ord(ch) + elif self.rx_state == 3: # Read fourth byte + self.rx_state = 0 + byte3 = ord(ch) + byte1 = self.rx_byte1 + byte2 = self.rx_byte2 + if DEBUG: + print ('Received', byte1, byte2, byte3) + if byte1 > 19: # Impossible command value + continue + if byte1 == 1 and self.set_L < 0: # reported inductor value + self.set_L = byte2 + if byte1 == 2 and self.set_C < 0: # reported capacitor value + self.set_C = byte2 + if byte1 == 3 and self.set_HiLoZ < 0: # reported Hi/Lo relay + self.set_HiLoZ = byte2 + if byte1 == 13: # Start standby + self.is_standby = 1 + elif byte1 == 14: # Start active + self.is_standby = 0 + self.param1[byte1] = byte2 + self.param2[byte1] = byte3 + def OpenPort(self): + if sys.platform == "win32": + tty_list = ("COM7", "COM8", "COM10", "COM11") + else: + tty_list = ("/dev/ttyUSB0", "/dev/ttyUSB1", "/dev/ttyUSB2") + for tty_name in tty_list: + try: + port = serial.Serial(port=tty_name, baudrate=9600, timeout=0) + except: + #traceback.print_exc() + pass + else: + port.setRTS(0) + time.sleep(0.1) + for i in range(3): + port.setRTS(1) + time.sleep(0.003) # Wait 3 milliseconds + port.write(chr(41)) + port.setRTS(0) + time.sleep(0.1) + chars = port.read(1024) + #print 'Got', tty_name, len(chars), repr(chars) + if chars == '?\r\n': # Wrong port + break + if "\xA5\x0B\x01\x20" in chars: + self.serial = port + break + if self.serial: + break + else: + port.close() + def open(self): + self.OpenPort() + if self.serial: + self.error = "Waiting for AT200PC" + # TunerLC is a list of (freq, L, C). Use -L for Low Z, +L for High Z. + # The first and last entry must have frequency 0 and 99999999. + self.TunerLC = [] + fp = open(self.TunerLC_fname, 'r') + for line in fp: + line = line.split() + f = int(line[0]) + l = int(line[1]) + c = int(line[2]) + self.TunerLC.append((f, l, c)) + fp.close() + def close(self): + if self.serial: + self.serial.close() + self.serial = None + if self.TunerLC_change: + fp = open(self.TunerLC_fname, 'w') + for f, l, c in self.TunerLC: + fp.write("%9d %4d %4d\n" % (f, l, c)) + fp.close() + def xxReqSetFreq(self, tx_freq): + # Set relays for this frequency. The frequency must exist in the tuner. + if self.serial and not self.standby and tx_freq > 1500000: + ticks = int(20480.0 / tx_freq * 1e6 + 0.5) + self.Write(chr(67) + chr((ticks & 0xFF00) >> 8) + chr(ticks & 0xFF)) + def SetTxFreq(self, tx_freq): + if tx_freq is None: + self.set_C = 0 + self.set_L = 0 + self.set_HiLoZ = 0 + return + self.tx_freq = tx_freq + if abs(self.old_tx_freq - tx_freq) < 20000: + d1 = tx_freq - self.tuning_F1 + d2 = tx_freq - self.tuning_F2 + if abs(d1) <= abs(d2): + self.tuning_diff = d1 + else: + self.tuning_diff = d2 + return # Ignore small tuning changes + self.old_tx_freq = tx_freq + i1 = 0 + i2 = len(self.TunerLC) - 1 + while 1: # binary partition + i = (i1 + i2) // 2 + if self.TunerLC[i][0] < tx_freq: + i1 = i + else: + i2 = i + if i2 - i1 <= 1: + break + # The correct setting is between i1 and i2; interpolate + F1 = self.TunerLC[i1][0] + F2 = self.TunerLC[i2][0] + L1 = self.TunerLC[i1][1] + L2 = self.TunerLC[i2][1] + C1 = self.TunerLC[i1][2] + C2 = self.TunerLC[i2][2] + frac = (float(tx_freq) - F1) / (F2 - F1) + C = C1 + (C2 - C1) * frac + self.set_C = int(C + 0.5) + L = L1 + (L2 - L1) * frac + if L < 0: + L = -L + self.set_HiLoZ = 1 + else: + self.set_HiLoZ = 0 + self.set_L = int(L + 0.5) + # Report the frequency difference + self.tuning_F1 = F1 + self.tuning_F2 = F2 + d1 = tx_freq - F1 + d2 = tx_freq - F2 + if abs(d1) <= abs(d2): + self.tuning_diff = d1 + else: + self.tuning_diff = d2 + def ChangeBand(self, band): + pass ##self.ReqSetFreq(self.tx_freq) + def OnSpot(self, level): + # level is -1 for Spot button Off; else the Spot level 0 to 1000. + if self.serial: + if level < 0: + self.live_update = 0 + elif not self.live_update: + self.live_update = 1 + self.timer = 999 + def OnAntTuner(self, text): # One of the tuner buttons was pressed + if self.serial: + if text == 'Tune': + if not self.standby: + #self.Write(chr(5)) # Request memory tune + self.Write(chr(6)) # Request full tune + self.set_C = -9 + self.set_L = -9 + self.set_HiLoZ = -9 + elif text == 'Save': + self.Write(chr(46)) + if self.set_HiLoZ == 0: # High Z + L = self.set_L + else: # Low Z + L = -self.set_L + for i in range(len(self.TunerLC)): # Record new freq and L/C + if abs(self.TunerLC[i][0] - self.tx_freq) < 1000: + self.TunerLC[i] = (self.tx_freq, L, self.set_C) + break + else: + self.TunerLC.append((self.tx_freq, L, self.set_C)) + self.TunerLC.sort() + self.TunerLC_change = True + elif text == 'L+': + self.set_L += 1 + elif text == 'L-': + self.set_L -= 1 + elif text == 'C+': + self.set_C += 1 + elif text == 'C-': + self.set_C -= 1 + +class App(wx.App): + def OnInit(self): + if sys.path[0] != "'.'": # Make sure the current working directory is on path + sys.path.insert(0, '.') + import quisk_conf_defaults as conf + import quisk_hardware_model as hardware + self.bottom_widgets = None + hardware.anttuner = AntennaTuner(self, conf) # Control the antenna tuner + hardware.v2filter = FilterBoxV2(self, conf) # Control V2 filter box + hardware.controlbox = ControlBox(self, conf) # Control my Station Control Box + self.main_frame = frame = StationControlGUI (None, hardware, self, conf) + frame.Open() + frame.Show() + return True + +if __name__ == '__main__': + App().MainLoop() diff --git a/n2adr/uhf_conf.py b/n2adr/uhf_conf.py new file mode 100644 index 0000000..8223829 --- /dev/null +++ b/n2adr/uhf_conf.py @@ -0,0 +1,65 @@ +# This is the config file for the VHF/UHF receiver and transmitter. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import sys, struct, socket, traceback + +settings_file_path = "../quisk_settings.json" + +DEBUG = 0 +if sys.platform == "win32": + n2adr_sound_pc_capt = 'Line In (Realtek High Definition Audio)' + n2adr_sound_pc_play = 'Speakers (Realtek High Definition Audio)' + n2adr_sound_usb_play = 'Primary' + n2adr_sound_usb_mic = 'Primary' + latency_millisecs = 150 + data_poll_usec = 20000 + favorites_file_path = "C:/pub/quisk_favorites.txt" +elif 0: # portaudio devices + name_of_sound_play = 'portaudio:CODEC USB' + microphone_name = "portaudio:AK5370" + latency_millisecs = 150 + data_poll_usec = 5000 + favorites_file_path = "/home/jim/pub/quisk_favorites.txt" +else: # alsa devices + n2adr_sound_pc_capt = 'alsa:ALC1150 Analog' + n2adr_sound_pc_play = 'alsa:ALC1150 Analog' + n2adr_sound_usb_play = 'alsa:USB Sound Device' + n2adr_sound_usb_mic = 'alsa:USB Sound Device' + latency_millisecs = 150 + data_poll_usec = 5000 + favorites_file_path = "/home/jim/pub/quisk_favorites.txt" + +name_of_sound_capt = "" +name_of_sound_play = n2adr_sound_pc_play +microphone_name = n2adr_sound_pc_capt + +playback_rate = 48000 +agc_off_gain = 80 +do_repeater_offset = True + +station_display_lines = 1 +# DX cluster telent login data, thanks to DJ4CM. +dxClHost = '' +#dxClHost = 'dxc.w8wts.net' +dxClPort = 7373 +user_call_sign = 'n2adr' + +bandLabels = ['6', '2', '1.25', '70cm', '33cm', '23cm', 'WWV'] +bandState['WWV'] = (19990000, 10000, 'AM') +BandEdge['WWV'] = (19500000, 20500000) + +use_rx_udp = 17 # Get ADC samples from UDP +rx_udp_ip = "192.168.1.199" # Sample source IP address +rx_udp_port = 0xAA53 # Sample source UDP port +#rx_clk38 = 38880000 - 30 # master clock frequency, 38880 kHz nominal +#rx_udp_clock = rx_clk38 * 32 // 2 // 9 # ADC sample rate in Hertz +sample_rate = 96000 # 96, 192, 384, 768, 1152 (for 69120/3/10) +display_fraction = 1.00 +fft_size_multiplier = 16 +tx_ip = "192.168.1.201" +tx_audio_port = 0xBC79 +add_imd_button = 1 +add_fdx_button = 1 diff --git a/n2adr/uhf_hardware.old b/n2adr/uhf_hardware.old new file mode 100644 index 0000000..857ff4f --- /dev/null +++ b/n2adr/uhf_hardware.old @@ -0,0 +1,498 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import sys, struct, socket, traceback + +from quisk_hardware_model import Hardware as BaseHardware +import _quisk as QS + +DEBUG = 0 + +class Adf4351: # class to hold adf4351 attributes + def __init__(self, receiver, clock, r_counter): + self.receiver = receiver + self.clock = clock + self.r_counter = r_counter + self.int_mode = 1 # integer one, fractional zero + self.band_sel_clock_div = 40 + self.aux_rf_out = 0b000 # enable 1/0, power 00 to 11 + self.frac_value = 0 + self.modulus = 23 + self.changed = 0 + +class Preamp: # Lone Wire Bus control of preamp for 2 meters and 70 cm + def __init__(self): + self.IP = '192.168.1.194' + self.PORT = 0x3A00 + 67 + # Create a socket for the Lone Wire Bus control of the preamp + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.connect((self.IP, self.PORT)) + self.socket.settimeout(0) + self.want_preamp = self.have_preamp = '\000' + def ChangeBand(self, band): + if band == '2': + self.want_preamp = '\001' + elif band == '70cm': + self.want_preamp = '\002' + else: + self.want_preamp = '\000' + #print ("ChangeBand", band) + def HeartBeat(self): + try: + data = self.socket.recv(4096) + except: + pass + else: + if len(data) == 1: + self.have_preamp = data + #print ("Got data 0x%X" % ord(data)) + if self.want_preamp != self.have_preamp: + self.socket.send(self.want_preamp) + #print ("Send data 0x%X" % ord(self.want_preamp)) + + +class Hardware(BaseHardware): + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.vfo_frequency = 52000000 + self.vfo_sample_rate = conf.sample_rate + self.vfo_test = 0 # JIM + self.tx_frequency = 0 + self.CorrectTxDc = { + '23cm':(1270.0, 0.167081, 0.150557), + '2':(146.0, 0.018772, 0.038658), + '33cm':(915.0, 0.140150, 0.051967), + '6':(52.0, 0.020590, 0.024557), + '70cm':(435.0, 0.004495, 0.096879), + '1.25':(223.5, 0.042958, 0.055212), + } + self.rx_clock38 = 38880000 - 30 # master clock frequency, 38880 kHz nominal + #rx_udp_clock = rx_clock38 * 32 // 2 // 9 # ADC sample rate in Hertz + self.rx_udp_clock_nominal = 69120000 # rate to display + self.tx_clock80 = 80000000 + 14 + self.firmware_version = None # firmware version is initially unknown + self.rx_udp_socket = None + self.tx_udp_socket = None + self.got_rx_udp_status = '' + self.got_tx_udp_status = '' + self.band = '' + self.rx_phase0 = self.rx_phase1 = 0 + self.tx_phase = 0 + self.button_PTT = 0 + self.mode_is_cw = 0 + self.scan_enable = 0 + self.scan_blocks = 0 + self.scan_samples = 1 + self.scan_phase = 0 + self.fft_scan_valid = 0.84 + self.preamp = Preamp() + self.Rx4351 = Adf4351(True, self.rx_clock38, 8) + self.Tx4351 = Adf4351(False, 10700000, 2) + self.Tx4351.aux_rf_out = 0b000 # enable aux RF out 0b111 or turn off 0b000 + self.decim3 = 10 + self.SetDecim(192000) + self.var_rates = ['31X', '19X', '9X', '5X', '3X', '2X', '1728', '1152', '768', '384', '192', '96', '48'] # supported sample rates as strings + self.index = 0 + self.repeater_freq = None + self.DcI, self.DcQ = (0.0, 0.0) + self.NewAdf4351(self.Rx4351, 146E6) + self.NewAdf4351(self.Tx4351, 146E6) + self.NewAd9951(52e6) + self.NewUdpStatus() + def ChangeFrequency(self, tx_freq, vfo_freq, source='', band='', event=None): + self.tx_frequency = tx_freq + if not self.Rx4351.frequency - 3E6 < vfo_freq < self.Rx4351.frequency + 3E6: + self.NewAdf4351(self.Rx4351, vfo_freq) + self.vfo_frequency = -1 + self.NewAd9951(tx_freq) + if abs(self.ad9951_freq - 10.7e6) > 15000: + self.NewAdf4351(self.Tx4351, tx_freq) + self.NewAd9951(tx_freq) + self.NewAd9951(tx_freq) + if self.vfo_frequency != vfo_freq: + self.vfo_frequency = vfo_freq + self.scan_deltaf = int(1152E3 * self.fft_scan_valid + 0.5) + self.scan_phase = int(1152.E3 * self.fft_scan_valid / self.conf.rx_udp_clock * 2.0**32 + 0.5) + self.scan_vfo0 = vfo_freq + rx_phase1 = int((vfo_freq - self.Rx4351.frequency) / self.conf.rx_udp_clock * 2.0**32 + 0.5) + if self.scan_enable: + self.scan_vfo0 = self.scan_vfo0 - self.scan_deltaf * (self.scan_blocks - 1) // 2 + rx_phase1 = rx_phase1 - int(self.scan_phase * (self.scan_blocks - 1) / 2.0 + 0.5) + self.rx_phase1 = rx_phase1 & 0xFFFFFFFF + rx_tune_freq = float(rx_phase1) * self.conf.rx_udp_clock / 2.0**32 + QS.change_rates(96000, tx_freq, self.vfo_sample_rate, vfo_freq) + QS.change_scan(self.scan_blocks, 1152000, self.fft_scan_valid, self.scan_vfo0, self.scan_deltaf) + if DEBUG: + #print( "vfo", vfo_freq, "adf4351", self.Rx4351.frequency, "phase", rx_phase1, "rx_tune", self.Rx4351.frequency - vfo_freq, rx_tune_freq) + #print ("VFO", self.Rx4351.frequency + rx_tune_freq) + print ("Change to Tx %d Vfo %d; VFO %.0f = adf4351_freq %.0f + rx_tune_freq %.0f" % (tx_freq, vfo_freq, + self.Rx4351.frequency + rx_tune_freq, self.Rx4351.frequency, rx_tune_freq)) + #print ("scan_enable %d, scan_blocks %d, scan_vfo0 %d, scan_deltaf %d" % (self.scan_enable, self.scan_blocks, self.scan_vfo0, self.scan_deltaf)) + else: + QS.change_rates(96000, tx_freq, self.vfo_sample_rate, self.vfo_frequency) + rx_phase0 = int((tx_freq - self.Rx4351.frequency) / self.conf.rx_udp_clock * 2.0**32 + 0.5) + self.rx_phase0 = rx_phase0 & 0xFFFFFFFF + self.NewUdpStatus() + if self.application.bottom_widgets: + Rx1 = self.Rx4351.frequency * 1e-6 + Rx2 = (self.ReturnVfoFloat() - self.Rx4351.frequency) * 1e-6 + t = "Rx Div %d; ADF4351 %.6f + rx_tune %.6f = %.6f Tx Adf4351 %.6f AD9951 %.6f" % ( + 2**self.Rx4351.rf_divider, Rx1, Rx2, Rx1 + Rx2, self.Tx4351.frequency * 1e-6, self.ad9951_freq * 1e-6) + self.application.bottom_widgets.UpdateText(t) + return tx_freq, vfo_freq + def RepeaterOffset(self, offset=None): # Change frequency for repeater offset during Tx + if offset is None: # Return True if frequency change is complete + self.HeartBeat() + return self.want_rx_udp_status[16:] == self.got_tx_udp_status[16:] + if offset == 0: # Change back to the original frequency + if self.repeater_freq is None: # Frequency was already reset + return self.want_rx_udp_status[16:] == self.got_tx_udp_status[16:] + self.ChangeFrequency(self.repeater_freq, self.vfo_frequency) + self.repeater_freq = None + else: # Shift to repeater input frequency + self.repeater_freq = self.tx_frequency + offset = int(offset * 1000) # Convert kHz to Hz + self.ChangeFrequency(self.tx_frequency + offset, self.vfo_frequency) + return False + def ReturnVfoFloat(self): # Return the accurate VFO as a float + rx_phase1 = int((self.vfo_frequency - self.Rx4351.frequency) / self.conf.rx_udp_clock * 2.0**32 + 0.5) + rx_tune_freq = float(rx_phase1) * self.conf.rx_udp_clock / 2.0**32 + return self.Rx4351.frequency + rx_tune_freq + def open(self): + ##self.application.config_screen.config.tx_phase.Enable(1) + # Create the proper broadcast address for rx_udp_ip. + nm = self.conf.rx_udp_ip_netmask.split('.') + ip = self.conf.rx_udp_ip.split('.') + nm = list(map(int, nm)) + ip = list(map(int, ip)) + bc = '' + for i in range(4): + x = (ip[i] | ~ nm[i]) & 0xFF + bc = bc + str(x) + '.' + self.broadcast_addr = bc[:-1] + # This socket is used for the Simple Network Discovery Protocol by AE4JY + self.socket_sndp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket_sndp.setblocking(0) + self.socket_sndp.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + self.sndp_request = chr(56) + chr(0) + chr(0x5A) + chr(0xA5) + chr(0) * 52 + self.sndp_rx_active = True + # conf.rx_udp_port is used for returning ADC samples + # conf.rx_udp_port + 1 is used for control + self.rx_udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.rx_udp_socket.setblocking(0) + self.rx_udp_socket.connect((self.conf.rx_udp_ip, self.conf.rx_udp_port + 1)) + # conf.tx_audio_port + 1 is used for control + if self.conf.tx_ip: + self.sndp_tx_active = True + self.tx_udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.tx_udp_socket.setblocking(0) + self.tx_udp_socket.connect((self.conf.tx_ip, self.conf.tx_audio_port + 1)) + else: + self.sndp_tx_active = False + QS.change_rates(96000, 0, 96000, 0) + self.application.test1Button.Enable(0) + return QS.open_rx_udp(self.conf.rx_udp_ip, self.conf.rx_udp_port) + def close(self): + if self.rx_udp_socket: + self.rx_udp_socket.close() + self.rx_udp_socket = None + if self.tx_udp_socket: + self.tx_udp_socket.close() + self.tx_udp_socket = None + def PrintStatus(self, msg, string): + print (msg, ' ', end=' ') + print (string[0:2], end=' ') + for c in string[2:]: + print ("%2X" % ord(c), end=' ') + print () + def GetFirmwareVersion(self): + return self.firmware_version + def ChangeMode(self, mode): + # mode is a string: "USB", "AM", etc. + if mode in ("CWL", "CWU"): + self.mode_is_cw = 1 + else: + self.mode_is_cw = 0 + self.NewUdpStatus() + def ChangeBand(self, band): + # band is a string: "60", "40", "WWV", etc. + self.band = band + try: + freq, DcI, DcQ = self.CorrectTxDc[band] + except KeyError: + DcI, DcQ = (0.0, 0.0) + self.NewUdpCorrect(DcI, DcQ) + self.preamp.ChangeBand(band) + def NewUdpCorrect(self, DcI, DcQ): + self.DcI = DcI + self.DcQ = DcQ + QS.set_udp_tx_correct(DcI, DcQ, 0.828) + self.NewUdpStatus() + def PrintUdpCorrect(self): + for band in self.CorrectTxDc: + freq, DcI, DcQ = self.CorrectTxDc[band] + print ("'%s':(%.1f, %.6f, %.6f)," % (band, freq, DcI, DcQ)) + def OnButtonPTT(self, event): + btn = event.GetEventObject() + if btn.GetValue(): # Turn the software key bit on or off + self.button_PTT = 1 + else: + self.button_PTT = 0 + QS.set_key_down(self.button_PTT) + self.NewUdpStatus() + def OnSpot(self, level): + # level is -1 for Spot button Off; else the Spot level 0 to 1000. + pass + def Sndp(self): # AE4JY Simple Network Discovery Protocol - attempt to set the FPGA IP address + try: + self.socket_sndp.sendto(self.sndp_request, (self.broadcast_addr, 48321)) + except: + if DEBUG: + traceback.print_exc() + return + for i in range(5): + try: + data = self.socket_sndp.recv(1024) + except: + break + if len(data) != 56: + continue + if data[5:17] == 'QuiskUHFR-v1': + ip = self.conf.rx_udp_ip.split('.') + ip = list(map(int, ip)) + ip = list(map(chr, ip)) + if data[37] == ip[3] and data[38] == ip[2] and data[39] == ip[1] and data[40] == ip[0]: + self.sndp_rx_active = False + if DEBUG: print("SNDP success for Rx") + else: + t = (data[0:4] + chr(2) + data[5:37] + ip[3] + ip[2] + ip[1] + ip[0] + + chr(0) * 12 + chr(self.conf.rx_udp_port & 0xFF) + chr(self.conf.rx_udp_port >> 8) + chr(0)) + self.socket_sndp.sendto(t, (self.broadcast_addr, 48321)) + elif data[5:17] == 'QuiskUHFT-v1': + if self.conf.tx_ip: + ip = self.conf.tx_ip.split('.') + ip = list(map(int, ip)) + ip = list(map(chr, ip)) + if data[37] == ip[3] and data[38] == ip[2] and data[39] == ip[1] and data[40] == ip[0]: + self.sndp_tx_active = False + if DEBUG: print("SNDP success for Tx") + else: + t = (data[0:4] + chr(2) + data[5:37] + ip[3] + ip[2] + ip[1] + ip[0] + + chr(0) * 12 + chr(self.conf.tx_audio_port & 0xFF) + chr(self.conf.tx_audio_port >> 8) + chr(0)) + self.socket_sndp.sendto(t, (self.broadcast_addr, 48321)) + def HeartBeat(self): + if self.sndp_rx_active or self.sndp_tx_active: + self.Sndp() + return # SNDP is required + for i in range(10): + try: # receive the Rx status if any + data = self.rx_udp_socket.recv(1024) + if DEBUG > 1: + self.PrintStatus(' gotRx ', data) + except: + break + else: + if data[0:2] == 'Sx': + self.got_rx_udp_status = data + if self.tx_udp_socket: + for i in range(10): + try: # receive the Tx status if any + data = self.tx_udp_socket.recv(1024) + if DEBUG > 1: + self.PrintStatus(' gotTx ', data) + except: + break + else: + if data[0:2] == 'Sx': + self.got_tx_udp_status = data + if self.want_rx_udp_status[16:] == self.got_rx_udp_status[16:]: # The first part returns information from the hardware + self.firmware_version = ord(self.got_rx_udp_status[2]) # Firmware version is returned here + self.Rx4351.changed = 0 + else: + if DEBUG > 1: + self.PrintStatus('HaveRx', self.got_rx_udp_status[0:20]) + self.PrintStatus('sendRx', self.want_rx_udp_status[0:20]) + try: + self.rx_udp_socket.send(self.want_rx_udp_status) + except: + #traceback.print_exc() + pass + if not self.tx_udp_socket: + pass + elif self.want_rx_udp_status[16:] == self.got_tx_udp_status[16:]: # The first part returns information from the hardware + self.Tx4351.changed = 0 + self.Tx9951_changed = 0 + else: + if DEBUG > 1: + self.PrintStatus('HaveTx', self.got_rx_udp_status[0:20]) + self.PrintStatus('sendTx', self.want_rx_udp_status[0:20]) + try: + self.tx_udp_socket.send(self.want_rx_udp_status) + except: + #traceback.print_exc() + pass + if 0: + self.rx_udp_socket.send('Qs') + self.preamp.HeartBeat() + def VarDecimGetChoices(self): # return text labels for the control + return self.var_rates + def VarDecimGetLabel(self): # return a text label for the control + return "Sample rate ksps" + def VarDecimGetIndex(self): # return the current index + return self.index + def VarDecimRange(self): + return (48000, 1152000) + def VarDecimSet(self, index=None): # set decimation, return sample rate + if index is None: # initial call to set decimation before the call to open() + rate = self.application.vardecim_set # May be None or from different hardware + try: + rate = rate // 1000 + if rate > 1152: + rate = 1152 + index = self.var_rates.index(str(rate)) + except: + rate = 192 + index = self.var_rates.index(str(rate)) + self.index = index + rate = self.var_rates[index] + if rate[-1] == 'X': + self.scan_enable = 1 + self.scan_blocks = int(rate[0:-1]) + self.scan_samples = self.application.fft_size + self.decim1 = 2 + self.decim2 = 3 + rate = 1152000 * self.scan_blocks + else: + self.scan_enable = 0 + self.scan_blocks = 0 + rate = int(rate) + rate = rate * 1000 + self.SetDecim(rate) + vfo = self.vfo_frequency + self.vfo_frequency = -1 + self.vfo_sample_rate = rate + self.ChangeFrequency(self.tx_frequency, vfo) + self.NewUdpStatus() + return rate + def SetDecim(self, rate): + # self.decim1, decim2, decim3 are the first, second, third decimations in the hardware + if rate >= 1152000: + self.decim1 = 2 + elif rate >= 192000: + self.decim1 = 3 + elif rate == 96000: + self.decim1 = 6 + else: + self.decim1 = 12 + self.decim2 = self.rx_udp_clock_nominal // rate // self.decim1 // self.decim3 + def NewUdpStatus(self): + # Start of 16 bytes sent to the hardware: + s = "Sx" # 0:2 Fixed string + s += chr(0) # 2 Version number is returned here + s += chr(0) # 3 + s += chr(0) * 12 # 4:16 + # Start of 80 bytes of data sent to the hardware: + s += chr( 6 - 1) # 0 Variable decimation less one channel 0 first + s += chr(12 - 1) # 1 Variable decimation less one channel 0 second + s += struct.pack(" 15000: + self.NewAdf4351(self.Tx4351, tx_freq) + self.NewAd9951(tx_freq) + self.NewAd9951(tx_freq) + if self.vfo_frequency != vfo_freq: + self.vfo_frequency = vfo_freq + self.scan_deltaf = int(1152E3 * self.fft_scan_valid + 0.5) + self.scan_phase = int(1152.E3 * self.fft_scan_valid / self.conf.rx_udp_clock * 2.0**32 + 0.5) + self.scan_vfo0 = vfo_freq + rx_phase1 = int((vfo_freq - self.Rx4351.frequency) / self.conf.rx_udp_clock * 2.0**32 + 0.5) + if self.scan_enable: + self.scan_vfo0 = self.scan_vfo0 - self.scan_deltaf * (self.scan_blocks - 1) // 2 + rx_phase1 = rx_phase1 - int(self.scan_phase * (self.scan_blocks - 1) / 2.0 + 0.5) + self.rx_phase1 = rx_phase1 & 0xFFFFFFFF + rx_tune_freq = float(rx_phase1) * self.conf.rx_udp_clock / 2.0**32 + QS.change_rates(96000, tx_freq, self.vfo_sample_rate, vfo_freq) + QS.change_scan(self.scan_blocks, 1152000, self.fft_scan_valid, self.scan_vfo0, self.scan_deltaf) + if DEBUG: + #print( "vfo", vfo_freq, "adf4351", self.Rx4351.frequency, "phase", rx_phase1, "rx_tune", self.Rx4351.frequency - vfo_freq, rx_tune_freq) + #print ("VFO", self.Rx4351.frequency + rx_tune_freq) + print ("Change to Tx %d Vfo %d; VFO %.0f = adf4351_freq %.0f + rx_tune_freq %.0f" % (tx_freq, vfo_freq, + self.Rx4351.frequency + rx_tune_freq, self.Rx4351.frequency, rx_tune_freq)) + #print ("scan_enable %d, scan_blocks %d, scan_vfo0 %d, scan_deltaf %d" % (self.scan_enable, self.scan_blocks, self.scan_vfo0, self.scan_deltaf)) + else: + QS.change_rates(96000, tx_freq, self.vfo_sample_rate, self.vfo_frequency) + rx_phase0 = int((tx_freq - self.Rx4351.frequency) / self.conf.rx_udp_clock * 2.0**32 + 0.5) + self.rx_phase0 = rx_phase0 & 0xFFFFFFFF + self.NewUdpStatus() + if self.application.bottom_widgets: + Rx1 = self.Rx4351.frequency * 1e-6 + Rx2 = (self.ReturnVfoFloat() - self.Rx4351.frequency) * 1e-6 + t = "Rx Div %d; ADF4351 %.6f + rx_tune %.6f = %.6f Tx Adf4351 %.6f AD9951 %.6f" % ( + 2**self.Rx4351.rf_divider, Rx1, Rx2, Rx1 + Rx2, self.Tx4351.frequency * 1e-6, self.ad9951_freq * 1e-6) + self.application.bottom_widgets.UpdateText(t) + return tx_freq, vfo_freq + def RepeaterOffset(self, offset=None): # Change frequency for repeater offset during Tx + if offset is None: # Return True if frequency change is complete + self.HeartBeat() + return self.want_rx_udp_status[16:] == self.got_tx_udp_status[16:] + if offset == 0: # Change back to the original frequency + if self.repeater_freq is None: # Frequency was already reset + return self.want_rx_udp_status[16:] == self.got_tx_udp_status[16:] + self.ChangeFrequency(self.repeater_freq, self.vfo_frequency) + self.repeater_freq = None + else: # Shift to repeater input frequency + self.repeater_freq = self.tx_frequency + offset = int(offset * 1000) # Convert kHz to Hz + self.ChangeFrequency(self.tx_frequency + offset, self.vfo_frequency) + return False + def ReturnVfoFloat(self): # Return the accurate VFO as a float + rx_phase1 = int((self.vfo_frequency - self.Rx4351.frequency) / self.conf.rx_udp_clock * 2.0**32 + 0.5) + rx_tune_freq = float(rx_phase1) * self.conf.rx_udp_clock / 2.0**32 + return self.Rx4351.frequency + rx_tune_freq + def open(self): + ##self.application.config_screen.config.tx_phase.Enable(1) + # Create the proper broadcast address for rx_udp_ip. + nm = self.conf.rx_udp_ip_netmask.split('.') + ip = self.conf.rx_udp_ip.split('.') + nm = list(map(int, nm)) + ip = list(map(int, ip)) + bc = '' + for i in range(4): + x = (ip[i] | ~ nm[i]) & 0xFF + bc = bc + str(x) + '.' + self.broadcast_addr = bc[:-1] + # This socket is used for the Simple Network Discovery Protocol by AE4JY + self.socket_sndp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket_sndp.setblocking(0) + self.socket_sndp.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + self.sndp_request = bytearray() + self.sndp_request.append(56) + self.sndp_request.append(0) + self.sndp_request.append(0x5A) + self.sndp_request.append(0xA5) + self.sndp_request += bytearray(52) + self.sndp_rx_active = True + # conf.rx_udp_port is used for returning ADC samples + # conf.rx_udp_port + 1 is used for control + self.rx_udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.rx_udp_socket.setblocking(0) + self.rx_udp_socket.connect((self.conf.rx_udp_ip, self.conf.rx_udp_port + 1)) + # conf.tx_audio_port + 1 is used for control + if self.conf.tx_ip: + self.sndp_tx_active = True + self.tx_udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.tx_udp_socket.setblocking(0) + self.tx_udp_socket.connect((self.conf.tx_ip, self.conf.tx_audio_port + 1)) + else: + self.sndp_tx_active = False + QS.change_rates(96000, 0, 96000, 0) + self.application.test1Button.Enable(0) + return QS.open_rx_udp(self.conf.rx_udp_ip, self.conf.rx_udp_port) + def close(self): + if self.rx_udp_socket: + self.rx_udp_socket.close() + self.rx_udp_socket = None + if self.tx_udp_socket: + self.tx_udp_socket.close() + self.tx_udp_socket = None + def PrintStatus(self, msg, string): + print (msg, ' ', end=' ') + print (string[0:2], end=' ') + for c in string[2:]: + print ("%2X" % ord(c), end=' ') + print () + def GetFirmwareVersion(self): + return self.firmware_version + def ChangeMode(self, mode): + # mode is a string: "USB", "AM", etc. + if mode in ("CWL", "CWU"): + self.mode_is_cw = 1 + else: + self.mode_is_cw = 0 + self.NewUdpStatus() + def ChangeBand(self, band): + # band is a string: "60", "40", "WWV", etc. + self.band = band + try: + freq, DcI, DcQ = self.CorrectTxDc[band] + except KeyError: + DcI, DcQ = (0.0, 0.0) + self.NewUdpCorrect(DcI, DcQ) + self.preamp.ChangeBand(band) + def NewUdpCorrect(self, DcI, DcQ): + self.DcI = DcI + self.DcQ = DcQ + QS.set_udp_tx_correct(DcI, DcQ, 0.828) + self.NewUdpStatus() + def PrintUdpCorrect(self): + for band in self.CorrectTxDc: + freq, DcI, DcQ = self.CorrectTxDc[band] + print ("'%s':(%.1f, %.6f, %.6f)," % (band, freq, DcI, DcQ)) + def OnButtonPTT(self, event): + btn = event.GetEventObject() + if btn.GetValue(): # Turn the software key bit on or off + self.button_PTT = 1 + else: + self.button_PTT = 0 + QS.set_key_down(self.button_PTT) + self.NewUdpStatus() + def OnSpot(self, level): + # level is -1 for Spot button Off; else the Spot level 0 to 1000. + pass + def Sndp(self): # AE4JY Simple Network Discovery Protocol - attempt to set the FPGA IP address + try: + self.socket_sndp.sendto(self.sndp_request, (self.broadcast_addr, 48321)) + except: + if DEBUG: + traceback.print_exc() + return + for i in range(5): + try: + data = self.socket_sndp.recv(1024) + except: + break + if len(data) != 56: + continue + if data[5:17] == b'QuiskUHFR-v1': + ip = self.conf.rx_udp_ip.split('.') + ip = list(map(int, ip)) + if data[37] == ip[3] and data[38] == ip[2] and data[39] == ip[1] and data[40] == ip[0]: + self.sndp_rx_active = False + if DEBUG: print("SNDP success for Rx") + else: + t = bytearray() + t += data[0:4] + t.append(2) + t += data[5:37] + t.append(ip[3]) + t.append(ip[2]) + t.append(ip[1]) + t.append(ip[0]) + t += bytearray(12) + t.append(self.conf.rx_udp_port & 0xFF) + t.append(self.conf.rx_udp_port >> 8) + t.append(0) + self.socket_sndp.sendto(t, (self.broadcast_addr, 48321)) + elif data[5:17] == b'QuiskUHFT-v1': + if self.conf.tx_ip: + ip = self.conf.tx_ip.split('.') + ip = list(map(int, ip)) + if data[37] == ip[3] and data[38] == ip[2] and data[39] == ip[1] and data[40] == ip[0]: + self.sndp_tx_active = False + if DEBUG: print("SNDP success for Tx") + else: + t = bytearray() + t += data[0:4] + t.append(2) + t += data[5:37] + t.append(ip[3]) + t.append(ip[2]) + t.append(ip[1]) + t.append(ip[0]) + t += bytearray(12) + t.append(self.conf.tx_audio_port & 0xFF) + t.append(self.conf.tx_audio_port >> 8) + t.append(0) + self.socket_sndp.sendto(t, (self.broadcast_addr, 48321)) + def HeartBeat(self): + if self.sndp_rx_active or self.sndp_tx_active: + if DEBUG: print ("Try SNDP", self.sndp_rx_active, self.sndp_tx_active) + self.Sndp() + return # SNDP is required + for i in range(10): + try: # receive the Rx status if any + data = self.rx_udp_socket.recv(1024) + if DEBUG > 1: + self.PrintStatus(' gotRx ', data) + except: + break + else: + if data[0:2] == b'Sx': + self.got_rx_udp_status = data + if self.tx_udp_socket: + for i in range(10): + try: # receive the Tx status if any + data = self.tx_udp_socket.recv(1024) + if DEBUG > 1: + self.PrintStatus(' gotTx ', data) + except: + break + else: + if data[0:2] == b'Sx': + self.got_tx_udp_status = data + if self.want_rx_udp_status[16:] == self.got_rx_udp_status[16:]: # The first part returns information from the hardware + self.firmware_version = self.got_rx_udp_status[2] # Firmware version is returned here + self.Rx4351.changed = 0 + else: + if DEBUG > 1: + self.PrintStatus('HaveRx', self.got_rx_udp_status[0:20]) + self.PrintStatus('sendRx', self.want_rx_udp_status[0:20]) + try: + self.rx_udp_socket.send(self.want_rx_udp_status) + except: + #traceback.print_exc() + pass + if not self.tx_udp_socket: + pass + elif self.want_rx_udp_status[16:] == self.got_tx_udp_status[16:]: # The first part returns information from the hardware + self.Tx4351.changed = 0 + self.Tx9951_changed = 0 + else: + if DEBUG > 1: + self.PrintStatus('HaveTx', self.got_rx_udp_status[0:20]) + self.PrintStatus('sendTx', self.want_rx_udp_status[0:20]) + try: + self.tx_udp_socket.send(self.want_rx_udp_status) + except: + #traceback.print_exc() + pass + if 0: + self.rx_udp_socket.send(b'Qs') + self.preamp.HeartBeat() + def VarDecimGetChoices(self): # return text labels for the control + return self.var_rates + def VarDecimGetLabel(self): # return a text label for the control + return "Sample rate ksps" + def VarDecimGetIndex(self): # return the current index + return self.index + def VarDecimRange(self): + return (48000, 1152000) + def VarDecimSet(self, index=None): # set decimation, return sample rate + if index is None: # initial call to set decimation before the call to open() + rate = self.application.vardecim_set # May be None or from different hardware + try: + rate = rate // 1000 + if rate > 1152: + rate = 1152 + index = self.var_rates.index(str(rate)) + except: + rate = 192 + index = self.var_rates.index(str(rate)) + self.index = index + rate = self.var_rates[index] + if rate[-1] == 'X': + self.scan_enable = 1 + self.scan_blocks = int(rate[0:-1]) + self.scan_samples = self.application.fft_size + self.decim1 = 2 + self.decim2 = 3 + rate = 1152000 * self.scan_blocks + else: + self.scan_enable = 0 + self.scan_blocks = 0 + rate = int(rate) + rate = rate * 1000 + self.SetDecim(rate) + vfo = self.vfo_frequency + self.vfo_frequency = -1 + self.vfo_sample_rate = rate + self.ChangeFrequency(self.tx_frequency, vfo) + self.NewUdpStatus() + return rate + def SetDecim(self, rate): + # self.decim1, decim2, decim3 are the first, second, third decimations in the hardware + if rate >= 1152000: + self.decim1 = 2 + elif rate >= 192000: + self.decim1 = 3 + elif rate == 96000: + self.decim1 = 6 + else: + self.decim1 = 12 + self.decim2 = self.rx_udp_clock_nominal // rate // self.decim1 // self.decim3 + def NewUdpStatus(self): + # Start of 16 bytes sent to the hardware: + s = bytearray() + s += b"Sx" # 0:2 Fixed string + s.append(0) # 2 Version number is returned here + s.append(0) # 3 + s += bytes(12) # 4:16 + # Start of 80 bytes of data sent to the hardware: + s.append( 6 - 1) # 0 Variable decimation less one channel 0 first + s.append(12 - 1) # 1 Variable decimation less one channel 0 second + s += struct.pack(" +#include +#include +#include +#include +#include +#include +#include +#include + +#define IMPORT_QUISK_API +#include "quisk.h" +#include "filter.h" + +// This module was written by Andrea Montefusco IW0HDV. + +typedef union { + struct { + int32_t i; + int32_t q; + } __attribute__((__packed__)) iq; + struct { + uint8_t i1; + uint8_t i2; + uint8_t i3; + uint8_t i4; + uint8_t q1; + uint8_t q2; + uint8_t q3; + uint8_t q4; + } __attribute__((__packed__)) ; +} iq_sample; + + +// buffer size for libperseus-sdr +const static int nb = 6; +const static int bs = 1024; + + +// This module uses the Python interface to import symbols from the parent _quisk +// extension module. It must be linked with import_quisk_api.c. See the documentation +// at the start of import_quisk_api.c. + +#define DEBUG 1 + +static int num_perseus = 0; +static perseus_descr *descr = 0; +static int sr = 48000; +static float freq = 7050000.0; +static int adc_dither = 0; +static int adc_preamp = 0; + +static void quisk_stop_samples(void); + +static const char *fname = "/tmp/quiskperseus"; +static int rfd = 0; +static int wfd = 0; +static int running = 0; +static int wb_filter = 0; + +// Called in a loop to read samples; called from the sound thread. +static int quisk_read_samples(complex double * cSamples) +{ + //fprintf (stderr, "r"); fflush(stderr); + + int n = read(rfd, cSamples, sizeof(complex double)*SAMP_BUFFER_SIZE); + //fprintf(stderr, "%d ", n); + if (n >= 0) + return n/sizeof(complex double); // return number of samples + else + return 0; +} + +// Called in a loop to write samples; called from the sound thread. +static int quisk_write_samples(complex double * cSamples, int nSamples) +{ + return 0; +} + + +// +// callback that writes in the output pipe IQ values as +// complex floating point +// +static int user_data_callback_c_f(void *buf, int buf_size, void *extra) +{ + // The buffer received contains 24-bit IQ samples (6 bytes per sample) + // Here we save the received IQ samples as 32 bit + // (msb aligned) integer IQ samples. + + uint8_t *samplebuf = (uint8_t*)buf; + int nSamples = buf_size/6; + int k; + iq_sample s; + + // the 24 bit data is scaled to a 32bit value (so that the machine's + // natural signed arithmetic will work) + for (k=0; k < nSamples; k++) { + s.i1 = s.q1 = 0; + s.i2 = *samplebuf++; + s.i3 = *samplebuf++; + s.i4 = *samplebuf++; + s.q2 = *samplebuf++; + s.q3 = *samplebuf++; + s.q4 = *samplebuf++; + + // move I/Q to complex number + complex double x = (double)(s.iq.i)*10 + (double)(s.iq.q)*10 * _Complex_I; + if (wfd > 0) { + int n = write(wfd, &x, sizeof(complex double)); + if (n<0 && ! -EAGAIN ) + fprintf(stderr, "perseus c: Can't write output file: %s, descriptor: %d\n", strerror(errno), wfd); + } + } + return 0; +} + + + +// Start sample capture; called from the sound thread. +static void quisk_start_samples(void) +{ + if (DEBUG) { fprintf (stderr, "perseus c: quisk_start_samples\n"); fflush(stderr); } + + int rc = mkfifo(fname, 0666); + + if ((rc == -1) && (errno != EEXIST)) { + perror("perseus c: Error creating the named pipe"); + } + + rfd = open(fname, O_RDONLY|O_NONBLOCK); + if (rfd < 0) { + fprintf(stderr, "perseus c: Can't open read FIFO (%s)\n", strerror(errno)); + } else { + if (DEBUG) fprintf(stderr, "perseus c: read FIFO (%d)\n", rfd); + } + wfd = open(fname, O_WRONLY|O_NONBLOCK); + if (wfd < 0) { + fprintf(stderr, "perseus c: Can't open write FIFO (%s)\n", strerror(errno)); + } else { + if (DEBUG) fprintf(stderr, "perseus c: write FIFO (%d)\n", wfd); + } + if (perseus_set_sampling_rate(descr, sr) < 0) { // specify the sampling rate value in Samples/second + fprintf(stderr, "perseus c: fpga configuration error: %s\n", perseus_errorstr()); + } else { + if (DEBUG) fprintf(stderr, "perseus c: sampling rate set to: %d\n", sr); + + // Re-enable preselection filters (WB_MODE Off) + perseus_set_ddc_center_freq(descr, freq, wb_filter); + // start sampling ops + if (perseus_start_async_input(descr, nb*bs, user_data_callback_c_f, 0)<0) { + fprintf(stderr, "perseus c: start async input error: %s\n", perseus_errorstr()); + } else { + if (DEBUG) fprintf(stderr, "perseus c: start async input\n"); + } + running = 1; + } +} + +// Stop sample capture; called from the sound thread. +static void quisk_stop_samples(void) +{ + if (DEBUG) { fprintf (stderr, "perseus c: quisk_stop_samples\n"); fflush(stderr); } + + // We stop the acquisition... + if (DEBUG) fprintf(stderr, "perseus c: stopping async data acquisition...\n"); + perseus_stop_async_input(descr); + running = 0; + // clearing FIFO... + close(rfd); + close(wfd); + unlink(fname); +} + + +// Called to close the sample source; called from the GUI thread. +static PyObject * close_device(PyObject * self, PyObject * args) +{ + if (DEBUG) fprintf (stderr, "perseus c: close_device\n"); + int sample_device; // for now one only Perseus can be managed + + if (!PyArg_ParseTuple (args, "i", &sample_device)) + return NULL; + + if (descr) { + // We stop the acquisition... + if (running) { + perseus_stop_async_input(descr); + running = 0; + } + perseus_close(descr); + descr = 0; + } + Py_INCREF (Py_None); + return Py_None; +} + +// Called to open the Perseus SDR device; called from the GUI thread. +static PyObject * open_device(PyObject * self, PyObject * args) +{ + char buf128[128] = "Capture Microtelecom Perseus HF receiver"; + eeprom_prodid prodid; + + if (DEBUG) { fprintf (stderr, "perseus c: open device (%d)\n", num_perseus); fflush(stderr); } + + // Check how many Perseus receivers are connected to the system + if (num_perseus == 0) num_perseus = perseus_init(); + if (DEBUG) fprintf(stderr, "perseus c: %d Perseus receivers found\n",num_perseus); + + if (num_perseus == 0) { + sprintf(buf128, "No Perseus receivers detected\n"); + perseus_exit(); + goto main_cleanup; + } + + // Open the first one... + if ((descr = perseus_open(0)) == NULL) { + sprintf(buf128, "error: %s\n", perseus_errorstr()); + fprintf(stderr, "perseus c: open error: %s\n", perseus_errorstr()); + goto main_cleanup; + } + + // Download the standard firmware to the unit + if (DEBUG) fprintf(stderr, "perseus c: Downloading firmware...\n"); + if (perseus_firmware_download(descr,NULL)<0) { + sprintf(buf128, "perseus c: firmware download error: %s", perseus_errorstr()); + goto main_cleanup; + } + // Dump some information about the receiver (S/N and HW rev) + if (perseus_is_preserie(descr, 0) == PERSEUS_SNNOTAVAILABLE) { + fprintf(stderr, "perseus c: The device is a preserie unit"); + } else { + if (perseus_get_product_id(descr,&prodid)<0) { + fprintf(stderr, "perseus c: get product id error: %s", perseus_errorstr()); + } else { + if (DEBUG) { + fprintf(stderr, "perseus c: Receiver S/N: %05d-%02hX%02hX-%02hX%02hX-%02hX%02hX - HW Release:%hd.%hd\n", + (uint16_t) prodid.sn, + (uint16_t) prodid.signature[5], + (uint16_t) prodid.signature[4], + (uint16_t) prodid.signature[3], + (uint16_t) prodid.signature[2], + (uint16_t) prodid.signature[1], + (uint16_t) prodid.signature[0], + (uint16_t) prodid.hwrel, + (uint16_t) prodid.hwver); + } + } + } + // Printing all sampling rates available ..... + { + int buf[BUFSIZ]; + + if (perseus_get_sampling_rates (descr, buf, sizeof(buf)/sizeof(buf[0])) < 0) { + fprintf(stderr, "perseus c: get sampling rates error: %s\n", perseus_errorstr()); + goto main_cleanup; + } else { + int i = 0; + while (buf[i]) { + if (DEBUG) fprintf(stderr, "perseus c: #%d: sample rate: %d\n", i, buf[i]); + i++; + } + } + } + + // Configure the receiver for 2 MS/s operations + if (DEBUG) fprintf(stderr, "perseus c: Configuring FPGA...\n"); + if (perseus_set_sampling_rate(descr, sr) < 0) { // specify the sampling rate value in Samples/second + //if (perseus_set_sampling_rate_n(descr, 0)<0) // specify the sampling rate value as ordinal in the vector + fprintf(stderr, "perseus c: fpga configuration error: %s\n", perseus_errorstr()); + goto main_cleanup; + } + + // ADC settings + perseus_set_adc (descr, adc_dither, adc_preamp); + + // Disable preselection filters (WB_MODE On) + //perseus_set_ddc_center_freq(descr, freq, 0); + //sleep(1); + // Re-enable preselection filters (WB_MODE Off) + perseus_set_ddc_center_freq(descr, freq, wb_filter); + + quisk_sample_source4(&quisk_start_samples, &quisk_stop_samples, &quisk_read_samples, &quisk_write_samples); + + if (DEBUG) { fprintf (stderr, "perseus c: quisk sample source callbacks established\n"); fflush(stderr); } + goto exit_success; + + + + main_cleanup: + return PyString_FromString("ERROR"); + + exit_success: + + return PyString_FromString(buf128); + + +} + +static PyObject * set_frequency(PyObject * self, PyObject * args) // Called from GUI thread +{ + float param; + + if (!PyArg_ParseTuple (args, "f", ¶m)) + return NULL; + if (DEBUG) + fprintf (stderr, "perseus c: set DDC frequency %lf WB filter:%d\n", param, wb_filter); + freq= param; + if (descr) perseus_set_ddc_center_freq(descr, freq, wb_filter); + + Py_INCREF (Py_None); + return Py_None; +} + + +static PyObject * set_input_filter(PyObject * self, PyObject * args) // Called from GUI thread +{ + int param; + + if (!PyArg_ParseTuple (args, "i", ¶m)) + return NULL; + if (DEBUG) + fprintf (stderr, "perseus c: set input filter %d\n", param); + wb_filter = param; + if (descr) perseus_set_ddc_center_freq(descr, freq, wb_filter); + + Py_INCREF (Py_None); + return Py_None; +} + + +static PyObject * set_sampling_rate(PyObject * self, PyObject * args) // Called from GUI thread +{ + int param; + + if (!PyArg_ParseTuple (args, "i", ¶m)) + return NULL; + + if (DEBUG) fprintf (stderr, "perseus c: Set sampling rate %d\n", param); + if (param < 48000) sr = param * 1000; + else sr = param; + + if (descr) { + if (running) { + if (DEBUG) fprintf(stderr, "perseus c: stop async input\n"); + perseus_stop_async_input(descr); + } + + // specify the sampling rate value in Samples/secon + if (perseus_set_sampling_rate(descr, sr) < 0) { + fprintf(stderr, "perseus c: fpga configuration error: %s\n", perseus_errorstr()); + } + + if (running) { + if (perseus_start_async_input(descr, nb*bs, user_data_callback_c_f, 0)<0) { + fprintf(stderr, "perseus c: start async input error: %s\n", perseus_errorstr()); + } else { + if (DEBUG) fprintf(stderr, "perseus c: start async input @%d\n", sr); + } + } + } else { + fprintf(stderr, "perseus c: trying to start async input with no device open\n"); + } + Py_INCREF (Py_None); + return Py_None; +} + + +static PyObject * set_attenuator(PyObject * self, PyObject * args) // Called from GUI thread +{ int param; + + if (!PyArg_ParseTuple (args, "i", ¶m)) + return NULL; + if (DEBUG) + fprintf (stderr, "perseus c: Set attenuator %d\n", param); + + if (descr) { + // specify the sampling rate value in Samples/secon + if (perseus_set_attenuator_n(descr, (int)(param / -10)) < 0) { + fprintf(stderr, "perseus c: fpga configuration error: %s\n", perseus_errorstr()); + } + } + Py_INCREF (Py_None); + return Py_None; +} + + +// Enable ADC Dither, Disable ADC Preamp +// perseus_set_adc(descr, true, false); + +static PyObject * set_adc_dither (PyObject * self, PyObject * args) // Called from GUI thread +{ int dither_; + + if (!PyArg_ParseTuple (args, "i", &dither_)) + return NULL; + if (DEBUG) + fprintf (stderr, "perseus c: Set ADC: dither %d\n", dither_); + + adc_dither = dither_; + if (descr) { + // specify the ADC dithering + if (perseus_set_adc(descr, adc_dither == 1, adc_preamp == 1) < 0) { + fprintf(stderr, "perseus c: ADC configuration error: %s\n", perseus_errorstr()); + } + } + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_adc_preamp (PyObject * self, PyObject * args) // Called from GUI thread +{ int preamp_; + + if (!PyArg_ParseTuple (args, "i", &preamp_)) + return NULL; + if (DEBUG) + fprintf (stderr, "perseus c: Set ADC: preamp: %d\n", preamp_); + + adc_preamp = preamp_; + if (descr) { + // specify the sampling rate value in Samples/secon + if (perseus_set_adc(descr, adc_dither == 1, adc_preamp == 1) < 0) { + fprintf(stderr, "perseus c: ADC configuration error: %s\n", perseus_errorstr()); + } + } + Py_INCREF (Py_None); + return Py_None; +} + + +static PyObject * deinit(PyObject * self, PyObject * args) // Called from dctor +{ + perseus_exit(); + + Py_INCREF (Py_None); + return Py_None; +} + + +// Functions callable from Python are listed here: +static PyMethodDef QuiskMethods[] = { + {"open_device", open_device, METH_VARARGS, "Open the hardware."}, + {"close_device", close_device, METH_VARARGS, "Close the hardware"}, + {"set_frequency", set_frequency, METH_VARARGS, "set frequency"}, + {"set_input_filter", set_input_filter, METH_VARARGS, "set input filter"}, + {"set_sampling_rate", set_sampling_rate, METH_VARARGS, "set sampling rate"}, + {"set_attenuator", set_attenuator, METH_VARARGS, "set attenuator"}, + {"set_adc_dither", set_adc_dither, METH_VARARGS, "set ADC dither"}, + {"set_adc_preamp", set_adc_preamp, METH_VARARGS, "set ADC preamplifier"}, + {"deinit", deinit, METH_VARARGS, "deinit"}, +// {"get_device_list", get_device_list, METH_VARARGS, "Return a list of Perseus SDR devices"}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +#if PY_MAJOR_VERSION < 3 +// Python 2.7: +// Initialization, and registration of public symbol "initperseus": +PyMODINIT_FUNC initperseus (void) +{ + if (Py_InitModule ("perseus", QuiskMethods) == NULL) { + fprintf(stderr, "perseus c: Py_InitModule failed!\n"); + return; + } + // Import pointers to functions and variables from module _quisk + if (import_quisk_api()) { + fprintf(stderr, "perseus c: Failure to import pointers from _quisk\n"); + return; //Error + } +} + +// Python 3: +#else +static struct PyModuleDef perseusmodule = { + PyModuleDef_HEAD_INIT, + "perseus", + NULL, + -1, + QuiskMethods +} ; + +PyMODINIT_FUNC PyInit_perseus(void) +{ + PyObject * m; + + m = PyModule_Create(&perseusmodule); + if (m == NULL) + return NULL; + + // Import pointers to functions and variables from module _quisk + if (import_quisk_api()) { + fprintf(stderr, "perseus c: Failure to import pointers from _quisk\n"); + return m; //Error + } + return m; +} +#endif + diff --git a/perseuspkg/quisk_hardware.py b/perseuspkg/quisk_hardware.py new file mode 100644 index 0000000..0301342 --- /dev/null +++ b/perseuspkg/quisk_hardware.py @@ -0,0 +1,189 @@ +# This is the hardware file to support radios accessed by the PerseusSDR interface. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import socket, traceback, time, math +import _quisk as QS +try: + from perseuspkg import perseus +except: + #traceback.print_exc() + perseus = None + print ("Error: Perseus package not found.\n") + +from quisk_hardware_model import Hardware as BaseHardware + +DEBUG = 1 + +# Define the name of the hardware and the items on the hardware screen (see quisk_conf_defaults.py): +################ Receivers PerseusSDR, The PerseusSDR interface to multiple hardware SDRs. +## hardware_file_name Hardware file path, rfile +# This is the file that contains the control logic for each radio. +#hardware_file_name = 'perseuspkg/quisk_hardware.py' + +## widgets_file_name Widget file path, rfile +# This optional file adds additional controls for the radio. +#widgets_file_name = 'perseuspkg/quisk_widgets.py' + +class Hardware(BaseHardware): + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + + self.rf_gain_labels = ('RF +0', 'RF -10', 'RF -20', 'RF -30') + self.antenna_labels = ('Wide Band', 'Band Filter') + + self.vardecim_index = 0 + self.fVFO = 0.0 # Careful, this is a float + if DEBUG: print ("__init__: %s" % conf) + self.rates = [ 48000, \ + 95000, \ + 96000, \ + 125000, \ + 192000, \ + 250000, \ + 500000, \ + 1000000, \ + 1600000, \ + 2000000 \ + ] + self.current_rate = 192000 + self.att = 0; + self.wb = 0 + + def __del__(self): + # try to clear hardware + if perseus: + perseus.close() + perseus.deinit() + + def get_hw (self): + return perseus + + def pre_open(self): + if DEBUG: print ("pre_open") + pass + + def set_parameter(self, *args): + pass + + def open(self): # Called once to open the Hardware + + if not perseus: + return "Perseus module not available" + + txt = perseus.open_device("perseus",2,3) + if DEBUG: print ("perseus hardware: open") + + return txt + + def close(self): # Called once to close the Hardware + if DEBUG: print ("perseus hardware: close") + if perseus: + perseus.close_device(1) + + def ChangeGain(self, rxtx): # rxtx is '_rx' or '_tx' + if not perseus: + return + if DEBUG: print ("perseus hardware: ChangeGain", rxtx) + pass + + def OnButtonRfGain(self, event): + #btn = event.GetEventObject() + n = event.GetEventObject().index + self.att = n * -10 + if DEBUG: print ("perseus hardware: OnButtonRfGain: %d new attenuation: %d" % (n, self.att)) + perseus.set_attenuator (self.att) + + def ChangeFrequency(self, tune, vfo, source='', band='', event=None): + fVFO = float(vfo) + if self.fVFO != fVFO: + self.fVFO = fVFO + perseus.set_frequency(fVFO) + return tune, vfo + + + def ReturnFrequency(self): + # Return the current tuning and VFO frequency. If neither have changed, + # you can return (None, None). This is called at about 10 Hz by the main. + # return (tune, vfo) # return changed frequencies + return None, None # frequencies have not changed + + def ReturnVfoFloat(self): + # Return the accurate VFO frequency as a floating point number. + # You can return None to indicate that the integer VFO frequency is valid. + return self.fVFO + +# def OnBtnFDX(self, fdx): # fdx is 0 or 1 +# pass +# +# def OnButtonPTT(self, event): +# pass +# +# def OnSpot(self, level): +# # level is -1 for Spot button Off; else the Spot level 0 to 1000. +# pass +# +# def ChangeMode(self, mode): # Change the tx/rx mode +# # mode is a string: "USB", "AM", etc. +# pass +# +# def ChangeBand(self, band): +# pass +# +# def HeartBeat(self): # Called at about 10 Hz by the main +# pass + + def ImmediateChange(self, name, value): + if DEBUG: print ("perseus hardware: ImmediateChange: perseus: name: %s value: %s" % (name, value)) + if name == 'perseus_setSampleRate_rx': + value = int(value) + self.application.OnBtnDecimation(rate=value) + perseus.set_sampling_rate(value) + self.curren_dec = value + + + def VarDecimGetChoices(self): # Not used to set sample rate + if DEBUG: print ("perseus hardware: VarDecimGetChoices") + return list(map(str, self.rates)) # convert integer to string + + def VarDecimGetLabel(self): # Return a text label for the decimation control. + return 'Sample rates: ' + + def VarDecimGetIndex(self): # Return the index 0, 1, ... of the current decimation. + for i in range(len(self.rates)): + if self.rates[i] == self.current_rate: + return i + return 0 + + def VarDecimSet(self, index=None): # Called when the control is operated; if index==None, called on startup. + print ("perseus hardware: VarDecimSet: index: %s" % (index)) + if index == None: + if DEBUG: print ("perseus hardware: VarDecimSet: current sampling rate: %d" % self.current_rate) + new_rate = self.current_rate = self.application.vardecim_set + else: + new_rate = self.rates[index] + + if DEBUG: print ("perseus hardware: VarDecimSet: New sampling rate: %d" % new_rate) + perseus.set_sampling_rate(int(new_rate)) + self.current_rate = int(new_rate) + + return int(new_rate) + + def VarDecimRange(self): # Return the lowest and highest sample rate. + if DEBUG: print ("perseus hardware: VarDecimRange: %s" % self.rates) + return (self.rates[0], self.rates[-1]) + + def OnButtonAntenna(self, event): + btn = event.GetEventObject() + n = btn.index + if DEBUG: print ("OnButtonAntenna: %d status: %d" % (n, self.wb)) + self.wb = n + perseus.set_input_filter (self.wb) + +# def StartSamples(self): # called by the sound thread +# print("perseus hardware: StartSamples") + +# def StopSamples(self): # called by the sound thread +# print("perseus hardware: StopSamples") diff --git a/perseuspkg/quisk_widgets.py b/perseuspkg/quisk_widgets.py new file mode 100644 index 0000000..e1b4721 --- /dev/null +++ b/perseuspkg/quisk_widgets.py @@ -0,0 +1,37 @@ +# Please do not change this widgets module for Quisk. Instead copy +# it to your own quisk_widgets.py and make changes there. +# +# This module is used to add extra widgets to the QUISK screen. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import math, wx + +class BottomWidgets: # Add extra widgets to the bottom of the screen + def __init__(self, app, hardware, conf, frame, gbs, vertBox): + self.config = conf + self.hardware = hardware + self.application = app + self.start_row = app.widget_row # The first available row + self.start_col = app.button_start_col # The start of the button columns + self.Widgets_0x06(app, hardware, conf, frame, gbs, vertBox) + + def Widgets_0x06(self, app, hardware, conf, frame, gbs, vertBox): + self.num_rows_added = 1 + start_row = self.start_row + b1 = app.QuiskCheckbutton(frame, self.OnADC_dither, 'ADC Dither') + gbs.Add(b1, (start_row, self.start_col), (1, 2), flag=wx.EXPAND) + b2 = app.QuiskCheckbutton(frame, self.OnADC_preamp, 'ADC Preamp') + gbs.Add(b2, (start_row, self.start_col + 2), (1, 2), flag=wx.EXPAND) + + def OnADC_dither(self, event): + btn = event.GetEventObject() + value = btn.GetValue() + self.hardware.get_hw().set_adc_dither (value) + + def OnADC_preamp(self, event): + btn = event.GetEventObject() + value = btn.GetValue() + self.hardware.get_hw().set_adc_preamp (value) diff --git a/perseuspkg/setup.py b/perseuspkg/setup.py new file mode 100644 index 0000000..ef48fc7 --- /dev/null +++ b/perseuspkg/setup.py @@ -0,0 +1,48 @@ +from distutils.core import setup, Extension +import sys + +module2 = Extension ('perseus', + libraries = ['m', 'perseus-sdr'], + sources = ['../import_quisk_api.c', 'perseus.c'], + include_dirs = ['.', '..'], + ) + +modulew2 = Extension ('perseus', + sources = ['../import_quisk_api.c', 'perseus.c'], + include_dirs = ['.', '..'], + libraries = ['WS2_32', 'perseus-sdr'], + ) + +if sys.platform == "win32": + Modules = [modulew2] +else: + Modules = [module2] + +setup (name = 'perseus', + version = '0.1', + description = 'perseus is an extension to Quisk to support Microtelecom Perseus SDR hardware', + long_description = """Microtelecom Perseus SDR HF receiver. +""", + author = 'Andrea Montefusco IW0HDV', + author_email = 'andrew@montefusco.com', + url = 'http://www.montefusco.com', + download_url = 'http://james.ahlstrom.name/quisk/', + packages = ['quisk.perseuspkg'], + package_dir = {'perseus' : '.'}, + ext_modules = Modules, + classifiers = [ + 'Development Status :: 6 - Mature', + 'Environment :: X11 Applications', + 'Environment :: Win32 (MS Windows)', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Natural Language :: English', + 'Operating System :: POSIX :: Linux', + 'Operating System :: Microsoft :: Windows', + 'Programming Language :: Python', + 'Programming Language :: C', + 'Topic :: Communications :: Ham Radio', + ], +) + + diff --git a/portaudio.py b/portaudio.py new file mode 100644 index 0000000..a9e57c3 --- /dev/null +++ b/portaudio.py @@ -0,0 +1,85 @@ +#! /usr/bin/python + +# Test for PortAudio devices using ctypes + +from __future__ import print_function + +import ctypes, ctypes.util + +class PaDeviceInfo (ctypes.Structure): + _fields_ = [ + ('structVersion', ctypes.c_int), + ('name', ctypes.c_char_p), + ('hostApi', ctypes.c_int), # PaHostApiIndex + ('maxInputChannels', ctypes.c_int), + ('maxOutputChannels', ctypes.c_int), + ('defaultLowInputLatency', ctypes.c_double), # PaTime + ('defaultLowOutputLatency', ctypes.c_double), # PaTime + ('defaultHighInputLatency', ctypes.c_double), # PaTime + ('defaultHighOutputLatency', ctypes.c_double), # PaTime + ('defaultSampleRate', ctypes.c_double), + ] + +class PaHostApiInfo (ctypes.Structure): + _fields_ = [ + ('structVersion', ctypes.c_int), + ('type', ctypes.c_int), # enum PaHostApiTypeId + ('name', ctypes.c_char_p), + ('deviceCount', ctypes.c_int), + ('defaultInputDevice', ctypes.c_int), + ('defaultOutputDevice', ctypes.c_int), + ] + +class PaStreamParameters (ctypes.Structure): + _fields_ = [ + ('device', ctypes.c_int), #PaDeviceIndex + ('channelCount', ctypes.c_int), + ('sampleFormat', ctypes.c_ulong), #PaSampleFormat + ('suggestedLatency', ctypes.c_double), # PaTime + ('hostApiSpecificStreamInfo', ctypes.c_void_p), + ] + +pa_name = ctypes.util.find_library("portaudio") +pa = ctypes.CDLL(pa_name) + +pa.Pa_GetDeviceInfo.restype = ctypes.POINTER(PaDeviceInfo) +pa.Pa_GetHostApiInfo.restype = ctypes.POINTER(PaHostApiInfo) +pa.Pa_GetVersionText.restype = ctypes.c_char_p + +inputParameters = PaStreamParameters (device=0, channelCount=2, + sampleFormat=2, suggestedLatency=0, # format 2 is paInt32 + hostApiSpecificStreamInfo=ctypes.c_void_p() ) + +outputParameters = PaStreamParameters (device=0, channelCount=2, + sampleFormat=2, suggestedLatency=0, # format 2 is paInt32 + hostApiSpecificStreamInfo=ctypes.c_void_p() ) + +print('Open', pa.Pa_Initialize()) +try: + print('Version', pa.Pa_GetVersion()) + print('Version Text', pa.Pa_GetVersionText()) + count = pa.Pa_GetDeviceCount() + print('NumDev', count) + for i in range(count): + pt_info = pa.Pa_GetDeviceInfo(i) + info = pt_info.contents + print("Device %2d, host api %s" % (i, pa.Pa_GetHostApiInfo(info.hostApi).contents.name)) + print(" Name %s" % info.name) + print(" Max inputs %d, Max outputs %d" % (info.maxInputChannels, info.maxOutputChannels)) + inputParameters.device = i + outputParameters.device = i + if info.maxInputChannels >= 2: + ptIn = ctypes.pointer(inputParameters) + else: + ptIn = ctypes.c_void_p() + if info.maxOutputChannels >= 2: + ptOut = ctypes.pointer(outputParameters) + else: + ptOut = ctypes.c_void_p() + print(" Speeds for 2-channel paInt32:", end=' ') + for speed in (44100, 48000, 96000, 192000): + if pa.Pa_IsFormatSupported(ptIn, ptOut, ctypes.c_double(speed)) == 0: + print(" %d" % speed, end=' ') + print() +finally: + print('Close', pa.Pa_Terminate()) diff --git a/quisk.c b/quisk.c new file mode 100644 index 0000000..b56884e --- /dev/null +++ b/quisk.c @@ -0,0 +1,6067 @@ +#define PY_SSIZE_T_CLEAN +#include +#include +#include +#include // Use native C99 complex type for fftw3 +#include +#include + +#ifdef MS_WINDOWS +#include +#include +static int cleanupWSA = 0; // Must we call WSACleanup() ? +HWND quisk_mainwin_handle; // Handle of the main window on Windows +#include +#else +#include +#include +#include +#include +#endif + +///static HANDLE CWkey_mutex; +///CWkey_mutex = CreateMutex(NULL, FALSE, NULL); +///if ( ! CWkey_mutex) +/// return; +///while (WaitForSingleObject(CWkey_mutex, 0) == WAIT_TIMEOUT) ; +///#include +///static pthread_mutex_t CWkey_mutex = PTHREAD_MUTEX_INITIALIZER; +///while (pthread_mutex_trylock(&CWkey_mutex) == EBUSY) ; + +#include "quisk.h" +#include "filter.h" +#include + +#ifdef MS_WINDOWS +CRITICAL_SECTION QuiskCriticalSection; +#endif + +#define DEBUG 0 + +// These are used for input/output of radio samples from/to a file. The SAMPLES_FROM_FILE is 0 for +// normal operation, 1 to record samples to a file, 2 to play samples from a file. Rate must be 48k. +#define SAMPLES_FROM_FILE 0 + +#define FM_FILTER_DEMPH 300.0 // Frequency of FM lowpass de-emphasis filter +#define AGC_DELAY 15 // Delay in AGC buffer in milliseconds +#define FFT_ARRAY_SIZE 4 // Number of FFTs +#define MULTIRX_FFT_MULT 8 // multirx FFT size is a multiple of graph size +#define MAX_RX_CHANNELS 3 // maximum paths to decode audio +#define MAX_RX_FILTERS 3 // maximum number of receiver filters +#define DGT_NARROW_FREQ 3000 // Use 6 ksps rate below this bandwidth +#define SQUELCH_FFT_SIZE 512 + +static int fft_error; // fft error count +typedef struct fftd { + fftw_complex * samples; // complex data for fft + int index; // position of next fft sample + int filled; // whether the fft is ready to run + int block; // block number 0, 1, ... +} fft_data; + +typedef struct mrx_fftd { // FFT data for sub-receivers + fftw_complex * samples; // complex data for fft of size multirx_fft_width + int index; // position of next fft sample +} mrx_fft_data; + +struct AgcState { // Store state information for the AGC + double max_out; // Must initialize to maximum output level 0.0 to 1.0. + int sample_rate; // Must initialize this to the sample rate or zero. + int buf_size; // Must initialize this to zero. + int index_read; + int index_start; + int is_clipping; + double themax; + double gain; + double delta; + double target_gain; + double time_release; + complex double * c_samp; +}; + +struct watfall_row_t { + int x_origin; + struct watfall_row_t * next_row; + struct watfall_row_t * prior_row; + uint8_t pixels[2]; // extend to size +} ; + +struct watfall_t { + uint8_t red[256]; + uint8_t green[256]; + uint8_t blue[256]; + int width; + int max_height; + struct watfall_row_t * current_row; +} ; + +static fft_data fft_data_array[FFT_ARRAY_SIZE]; // Data for several FFTs +static int fft_data_index = 0; // Write the current samples to this FFT +static fftw_plan quisk_fft_plan; +static char fftw_wisdom_name[QUISK_SC_SIZE]; // wisdom patch provided by Eoin Mcloughlin, EI7HSB +static double * fft_window; // Window for FFT data +static double * current_graph; // current graph data as returned + +static PyObject * QuiskError; // Exception for this module +static PyObject * pyApp; // Application instance +static int fft_size; // size of fft, e.g. 1024 +int data_width; // number of points to return as graph data; fft_size * n +static int graph_width; // width of the graph in pixels +rx_mode_type rxMode; // 0 to 13: CWL, CWU, LSB, USB, AM, FM, EXT, DGT-U, DGT-L, DGT-IQ, IMD, FDV-U, FDV-L, DGT-FM +int quisk_noise_blanker; // noise blanker level, 0 for off +int quiskTxHoldState; // hold Tx until the repeater frequency shift is complete +int quisk_is_vna; // zero for normal program, one for the VNA program +static int py_sample_rx_bytes=2; // number of bytes in each I or Q sample: 1, 2, 3, or 4 +static int py_sample_rx_endian; // order of sample array: 0 == little endian; 1 == big endian +static int py_bscope_bytes; +static int py_bscope_endian; +static int quisk_auto_notch; // auto notch control +PyObject * quisk_pyConfig=NULL; // Configuration module instance +static int graphX; // Origin of first X value for graph data +static int graphY; // Origin of 0 dB for graph data +static double graphScale; // Scale factor for graph +static complex double testtonePhase; // Phase increment for test tone +double quisk_audioVolume; // Audio output level, 0.0 to 1.0 +double quisk_ctcss_freq; // CTCSS repeater access tone frequency in Hertz, or zero +static double cFilterI[MAX_RX_FILTERS][MAX_FILTER_SIZE]; // Digital filter coefficients for receivers +static double cFilterQ[MAX_RX_FILTERS][MAX_FILTER_SIZE]; // Digital filter coefficients +static int sizeFilter; // Number of coefficients for filters +int quisk_isFDX; // Are we in full duplex mode? +static int filter_bandwidth[MAX_RX_FILTERS]; // Current filter bandwidth in Hertz +static int filter_start_offset; // Current filter +/- start offset frequency from rx_tune_freq in Hertz for filter zero +static int quisk_decim_srate; // Sample rate after decimation +static int quisk_filter_srate=48000; // Frequency for filters +static int split_rxtx; // Are we in split rx/tx mode? +static int kill_audio; // Replace radio sound with silence +static int quisk_transmit_mode; // Set transmit mode. No hang time on release. +static int fft_sample_rate; // Sample rate on the graph (not the audio channel) -fft_srate/2 < freq < +fft_srate/2 +static int scan_blocks=0; // Number of FFT blocks for scan; or zero +static int scan_sample_rate=1; // Sample rate to use while scanning +static double scan_valid=0.84; // Fraction of each FFT block that is valid +static int scan_vfo0; +static int scan_deltaf; +static int graph_refresh; // Graph refresh rate from the config file +static int multiple_sample_rates=0; // Hardware supports multiple different sample rates +static int vfo_screen; // The VFO (center) frequency on the FFT screen +static int vfo_audio; // VFO frequency for the audio channel +static int is_PTT_down; // state 0/1 of PTT button +static int sample_bytes=3; // number of bytes in each I or Q sample +static int waterfall_scroll_mode = 1; // draw the first lines multiple times +static int quisk_use_sidetone; // is there a sidetone volume control? +static int hl2_txbuf_errors; // errors in the Hermes-Lite2 Tx buffer +static int hl2_txbuf_state; // state machine for errors in the Hermes-Lite2 Tx buffer + +static complex double PySampleBuf[SAMP_BUFFER_SIZE]; // buffer for samples returned from Python +static int PySampleCount; // count of samples in buffer + +static int multirx_data_width; // width of graph data to return +static int multirx_fft_width; // size of FFT samples +int quisk_multirx_count; // number of additional receivers zero or 1, 2, 3, ... +static int quisk_multirx_state; // state of hermes receivers +static mrx_fft_data multirx_fft_data[QUISK_MAX_SUB_RECEIVERS]; // FFT data for the sub-receivers +static int multirx_fft_next_index; // index of the receiver for the next FFT to return +static double multirx_fft_next_time; // timing interval for multirx FFT +static int multirx_fft_next_state; // state of multirx FFT: 0 == filling, 1 == ready, 2 == done +static fftw_plan multirx_fft_next_plan; // fftw3 plan for multirx FFTs +static fftw_complex * multirx_fft_next_samples; // sample buffer for multirx FFT +static int multirx_play_method; // 0== both, 1==left, 2==right +static int multirx_play_channel = -1; // index of the channel to play; or -1 +static int multirx_freq[QUISK_MAX_SUB_RECEIVERS]; // tune frequency for channel +static int multirx_mode[QUISK_MAX_SUB_RECEIVERS]; // mode CW, SSB, etc. for channel + +static complex double * multirx_cSamples[QUISK_MAX_SUB_RECEIVERS]; // samples for the sub-receivers +static int multirx_sample_size; // current size of the sub-receiver sample array + +double quisk_sidetoneVolume; // Audio output level of the CW sidetone, 0.0 to 1.0 +static complex double sidetonePhase; // Phase increment for sidetone +int quisk_sidetoneCtrl; // sidetone control value 0 to 1000 +int quisk_sidetoneFreq; // frequency in hertz for the sidetone +int quisk_start_cw_delay = 15; // milliseconds to delay output on CW key down +int quisk_start_ssb_delay = 100; // milliseconds to discard output on SSB etc. key down +static int maximum_tx_secs; // Failsafe timeout for Tx in seconds +static int key_is_down = 0; // internal key state up or down + +static double agcReleaseGain=80; // AGC maximum gain +static double agc_release_time = 1.0; // Release time in seconds +static double squelch_level=-999.0; // setting of FM squelch control +static int ssb_squelch_enabled; +static int ssb_squelch_level; +static int quisk_invert_spectrum = 0; // Invert the input RF spectrum +static void process_agc(struct AgcState *, complex double *, int, int); + +static double Smeter; // Measured RMS signal strength +static int rx_tune_freq; // Receive tuning frequency as +/- sample_rate / 2, including RIT +int quisk_tx_tune_freq; // Transmit tuning frequency as +/- sample_rate / 2 +static int rit_freq; // RIT frequency in Hertz + +#define RX_UDP_SIZE 1442 // Expected size of UDP samples packet +static SOCKET rx_udp_socket = INVALID_SOCKET; // Socket for receiving ADC samples from UDP +int quisk_rx_udp_started = 0; // Have we received any data yet? +int quisk_using_udp = 0; // Are we using rx_udp_socket? No longer used, but provided for backward compatibility. +static double rx_udp_gain_correct = 0; // Small correction for different decimation rates +static double rx_udp_clock; // Clock frequency for UDP samples +int quisk_use_rx_udp; // from the config file +play_state_t quisk_play_state; + +static int is_little_endian; // Test byte order; is it little-endian? +unsigned char quisk_pc_to_hermes[17 * 4]; // data to send from PC to Hermes hardware +unsigned char quisk_hermeslite_writequeue[5]; // One-time writes to Hermes-Lite +unsigned int quisk_hermeslite_writepointer = 0; +static unsigned char quisk_hermes_to_pc[5 * 4]; // data received from the Hermes hardware +static unsigned char quisk_hermeslite_response[5]; // response from Hermes-Lite commands +unsigned int quisk_hermes_code_version = -1; // code version returned by the Hermes hardware +unsigned int quisk_hermes_board_id = -1; // board ID returned by the Hermes hardware +static double hermes_temperature; // average temperature +static double hermes_fwd_power; // average forward power +static double hermes_rev_power; // average reverse power +static double hermes_fwd_peak; // peak forward power +static double hermes_rev_peak; // peak reverse power +static double hermes_pa_current; // average power amp current +static int hermes_count_temperature; // number of temperature samples +static int hermes_count_current; // number of current samples +static int hardware_ptt; // hardware PTT switch + +int quisk_hardware_cwkey; // hardware CW key from UDP or USB +static int old_hardware_cwkey; // previous hardware CW key +int quisk_remote_cwkey; // remote CW key (sent from control head) +static int old_remote_cwkey; // previous remote CW key + +enum quisk_rec_state quisk_record_state = IDLE; +static float * quisk_record_buffer; +static int quisk_record_bufsize; +static int quisk_record_index; +static int quisk_play_index; +static int quisk_mic_index; +static int quisk_record_full; + +// These are used to measure the frequency of a continuous RF signal. +static void measure_freq(complex double *, int, int); +static double measured_frequency; +static int measure_freq_mode=0; + +//These are used to measure the demodulated audio voltage +static double measured_audio; +static double measure_audio_sum; +static int measure_audio_count; +static int measure_audio_time=1; + +// This is used to measure the squelch level +static struct _MeasureSquelch { + int squelch_active; + // These are used for FM squelch + double rf_sum; + double squelch; + int rf_count; + // These are used for SSB squelch + double * in_fft; + int index; + int sq_open; +} MeasureSquelch[MAX_RX_CHANNELS]; + +// These are used for playback of a WAV file. +static int wavStart; // Sound data starts at this offset +// Open two WAV files with the same name. Two wavFp are needed because the same file is used on asynchronous streams. +static FILE * wavFpSound; // File pointer to play the Audio WAV file or Samples WAV file +static FILE * wavFpMic; // File pointer to play the same Audio WAV file replacing the Microphone sound +int quisk_close_file_play; + +// These are used for bandscope data from Hermes +static int enable_bandscope = 1; +static fftw_plan bandscopePlan=NULL; +static unsigned int bandscopeState = 0; +static unsigned int bandscopeBlockCount = 4; +static int bandscope_size = 0; +static double * bandscopeSamples = NULL; // bandscope samples are normalized to max 1.0 with bandscopeScale +static double bandscopeScale = 32768; // maximum value of the samples sent to the bandscope +static double * bandscopeWindow = NULL; +static double * bandscopeAverage = NULL; +static double * bandscopePixels = NULL; +static complex double * bandscopeFFT = NULL; +static double hermes_adc_level = 0.0; // maximum bandscope sample from the ADC, 0.0 to 1.0 + +#if SAMPLES_FROM_FILE +static struct QuiskWav hWav; + +int QuiskWavWriteOpen(struct QuiskWav * hWav, char * file_name, short format, short nChan, short bytes, int rate, double scale) +{ + unsigned int u; // must be 4 bytes + unsigned short s; // must be 2 bytes + + hWav->format = format; + hWav->nChan = nChan; + hWav->bytes_per_sample = bytes; + hWav->sample_rate = rate; + hWav->scale = scale; + hWav->samples = 0; // number of samples written + hWav->fp = fopen(file_name, "wb"); + if ( ! hWav->fp) + return 0; + if (format == 0) // RAW format - no header + return 1; + if (fwrite("RIFF", 1, 4, hWav->fp) != 4) { + fclose(hWav->fp); + hWav->fp = NULL; + return 0; + } + if (format == 1) // PCM + u = 36; + else + u = 50; + fwrite(&u, 4, 1, hWav->fp); + fwrite("WAVE", 1, 4, hWav->fp); + fwrite("fmt ", 1, 4, hWav->fp); + if (format == 1) // PCM + u = 16; + else + u = 18; + fwrite(&u, 4, 1, hWav->fp); + fwrite(&format, 2, 1, hWav->fp); // format + fwrite(&nChan, 2, 1, hWav->fp); // number of channels + fwrite(&rate, 4, 1, hWav->fp); // sample rate + u = rate * bytes * nChan; + fwrite(&u, 4, 1, hWav->fp); + s = bytes * nChan; + fwrite(&s, 2, 1, hWav->fp); + s = bytes * 8; + fwrite(&s, 2, 1, hWav->fp); + if (format != 1) { + s = 0; + fwrite(&s, 2, 1, hWav->fp); + fwrite("fact", 1, 4, hWav->fp); + u = 4; + fwrite(&u, 4, 1, hWav->fp); + u = 0; + fwrite(&u, 4, 1, hWav->fp); + } + fwrite("data", 1, 4, hWav->fp); + u = 0; + fwrite(&u, 4, 1, hWav->fp); + return 1; +} + +void QuiskWavWriteC(struct QuiskWav * hWav, complex double * cSamples, int nSamples) +{ // Record the samples to a WAV file, two float samples I/Q. Always use IEEE format 3. + int j; // TODO: add other formats + float samp; // must be 4 bytes + + if ( ! hWav->fp) + return; + // append the samples + hWav->samples += (unsigned int)nSamples; + fseek(hWav->fp, 0, SEEK_END); // seek to the end + for (j = 0; j < nSamples; j++) { + samp = creal(cSamples[j]) * hWav->scale; + fwrite(&samp, 4, 1, hWav->fp); + samp = cimag(cSamples[j]) * hWav->scale; + fwrite(&samp, 4, 1, hWav->fp); + } + // write the sizes to the header + QuiskWavWriteD(hWav, NULL, 0); +} + +void QuiskWavWriteD(struct QuiskWav * hWav, double * dSamples, int nSamples) +{ // Record the samples to a file, one channel. + int j; + float samp; // must be 4 bytes + unsigned int u; // must be 4 bytes + int i; // must be 4 bytes + char c; // must be 1 byte + short s; // must be 2 bytes + + if ( ! hWav->fp) + return; + // append the samples + hWav->samples += (unsigned int)nSamples; + fseek(hWav->fp, 0, SEEK_END); // seek to the end + if ( ! dSamples) { + ; // Only update the header + } + else if (hWav->format == 3) { // float + for (j = 0; j < nSamples; j++) { + samp = dSamples[j] * hWav->scale; + fwrite(&samp, 4, 1, hWav->fp); + } + } + else { // PCM integer + switch (hWav->bytes_per_sample) { + case 1: + for (j = 0; j < nSamples; j++) { + c = (char)(dSamples[j] * hWav->scale); + fwrite(&c, 1, 1, hWav->fp); + } + break; + case 2: + for (j = 0; j < nSamples; j++) { + s = (short)(dSamples[j] * hWav->scale); + fwrite(&s, 2, 1, hWav->fp); + } + break; + case 4: + for (j = 0; j < nSamples; j++) { + i = (int)(dSamples[j] * hWav->scale); + fwrite(&i, 4, 1, hWav->fp); + } + break; + } + } + // write the sizes to the header + if (hWav->format == 0) { // RAW format + ; + } + else if (hWav->format == 3) { // float + fseek(hWav->fp, 54, SEEK_SET); // seek from the beginning + u = hWav->bytes_per_sample * hWav->nChan * hWav->samples; + fwrite(&u, 4, 1, hWav->fp); + fseek(hWav->fp, 4, SEEK_SET); + u += 50 ; + fwrite(&u, 4, 1, hWav->fp); + fseek(hWav->fp, 46, SEEK_SET); + u = hWav->samples * hWav->nChan; + fwrite(&u, 4, 1, hWav->fp); + } + else { + fseek(hWav->fp, 40, SEEK_SET); + u = hWav->bytes_per_sample * hWav->nChan * hWav->samples; + fwrite(&u, 4, 1, hWav->fp); + fseek(hWav->fp, 4, SEEK_SET); + u += 36 ; + fwrite(&u, 4, 1, hWav->fp); + } + if (hWav->samples > 536870000) // 2**32 / 8 + QuiskWavClose(hWav); +} + +int QuiskWavReadOpen(struct QuiskWav * hWav, char * file_name, short format, short nChan, short bytes, int rate, double scale) +{ // TODO: Get parameters from the WAV file header. + char name[5]; + int size; + + hWav->format = format; + hWav->nChan = nChan; + hWav->bytes_per_sample = bytes; + hWav->sample_rate = rate; + hWav->scale = scale; + hWav->fp = fopen(file_name, "rb"); + if (!hWav->fp) + return 0; + if (hWav->format == 0) { // RAW format + fseek(hWav->fp, 0, SEEK_END); // seek to the end + hWav->fpEnd = ftell(hWav->fp); + hWav->fpStart = hWav->fpPos = 0; + return 1; + } + hWav->fpEnd = 0; + while (1) { // WAV format + if (fread (name, 4, 1, hWav->fp) != 1) + return 0; + if (fread (&size, 4, 1, hWav->fp) != 1) + return 0; + name[4] = 0; + //QuiskPrintf("name %s size %d\n", name, size); + if (!strncmp(name, "RIFF", 4)) + fseek (hWav->fp, 4, SEEK_CUR); // Skip "WAVE" + else if (!strncmp(name, "data", 4)) { // sound data starts here + hWav->fpStart = ftell(hWav->fp); + hWav->fpEnd = hWav->fpStart + size; + hWav->fpPos = hWav->fpStart; + break; + } + else // Skip other records + fseek (hWav->fp, size, SEEK_CUR); + } + //QuiskPrintf("start %d end %d\n", hWav->fpStart, hWav->fpEnd); + if (!hWav->fpEnd) { // Failure to find "data" record + fclose(hWav->fp); + hWav->fp = NULL; + return 0; + } + return 1; +} + +void QuiskWavReadC(struct QuiskWav * hWav, complex double * cSamples, int nSamples) +{ // Always uses format 3. TODO: add other formats. + int i; + float fi, fq; + double di, dq; + +#if 0 + double noise; + noise = 1.6E6; + for (i = 0; i < nSamples; i++) { + di = ((float)random() / RAND_MAX - 0.5) * noise; + dq = ((float)random() / RAND_MAX - 0.5) * noise; + cSamples[i] = di + I * dq; + } +#endif + if (hWav->fp && nSamples > 0) { + fseek (hWav->fp, hWav->fpPos, SEEK_SET); + for (i = 0; i < nSamples; i++) { + if (fread(&fi, 4, 1, hWav->fp) != 1) + break; + if (fread(&fq, 4, 1, hWav->fp) != 1) + break; + di = fi * hWav->scale; + dq = fq * hWav->scale; + cSamples[i] += di + I * dq; + hWav->fpPos += hWav->bytes_per_sample * hWav->nChan; + if (hWav->fpPos >= hWav->fpEnd) + hWav->fpPos = hWav->fpStart; + } + } +} + +void QuiskWavReadD(struct QuiskWav * hWav, double * dSamples, int nSamples) +{ + int j; + float samp; + int i; // must be 4 bytes + char c; // must be 1 byte + short s; // must be 2 bytes + + if (hWav->fp && nSamples > 0) { + fseek (hWav->fp, hWav->fpPos, SEEK_SET); + for (j = 0; j < nSamples; j++) { + if (hWav->format == 3) { // float + if (fread(&samp, 4, 1, hWav->fp) != 1) + return; + } + else { // PCM integer + switch (hWav->bytes_per_sample) { + case 1: + if (fread(&c, 1, 1, hWav->fp) != 1) + return; + samp = c; + break; + case 2: + if (fread(&s, 2, 1, hWav->fp) != 1) + return; + samp = s; + break; + case 4: + if (fread(&i, 4, 1, hWav->fp) != 1) + return; + samp = i; + break; + } + } + dSamples[j] = samp * hWav->scale; + hWav->fpPos += hWav->bytes_per_sample * hWav->nChan; + if (hWav->fpPos >= hWav->fpEnd) + hWav->fpPos = hWav->fpStart; + } + } +} + +void QuiskWavClose(struct QuiskWav * hWav) +{ + if (hWav->fp) { + fclose(hWav->fp); + hWav->fp = NULL; + } +} +#endif + +// These are used for digital voice codecs +ty_dvoice_codec_rx pt_quisk_freedv_rx; +ty_dvoice_codec_tx pt_quisk_freedv_tx; + +void quisk_dvoice_freedv(ty_dvoice_codec_rx rx, ty_dvoice_codec_tx tx) +{ + pt_quisk_freedv_rx = rx; + pt_quisk_freedv_tx = tx; +} +#if 0 +static int fFracDecim(double * dSamples, int nSamples, double fdecim) +{ // fractional decimation by fdecim > 1.0 + int i, nout; + double xm0, xm1, xm2, xm3; + static double dindex = 1; + static double y0=0, y1=0, y2=0, y3=0; + static int in=0, out=0; + + in += nSamples; + nout = 0; + for (i = 0; i < nSamples; i++) { + y3 = dSamples[i]; + if (dindex < 1 || dindex >= 2.4) + QuiskPrintf ("dindex %.5f fdecim %.8f\n", dindex, fdecim); + if (dindex < 2) { +#if 0 + dSamples[nout++] = (1 - (dindex - 1)) * y1 + (dindex - 1) * y2; +#else + xm0 = dindex - 0; + xm1 = dindex - 1; + xm2 = dindex - 2; + xm3 = dindex - 3; + dSamples[nout++] = xm1 * xm2 * xm3 * y0 / -6.0 + xm0 * xm2 * xm3 * y1 / 2.0 + + xm0 * xm1 * xm3 * y2 / -2.0 + xm0 * xm1 * xm2 * y3 / 6.0; +#endif + out++; + dindex += fdecim - 1; + y0 = y1; + y1 = y2; + y2 = y3; + } + else { + if (dindex > 2.5) QuiskPrintf ("Skip at %.2f\n", dindex); + y0 = y1; + y1 = y2; + y2 = y3; + dindex -= 1; + } + } + //QuiskPrintf ("in %d out %d\n", in, out); + return nout; +} +#endif +static int cFracDecim(complex double * cSamples, int nSamples, double fdecim) +{ +// Fractional decimation of I/Q signals works poorly because it introduces aliases and birdies. + int i, nout; + double xm0, xm1, xm2, xm3; + static double dindex = 1; + static complex double c0=0, c1=0, c2=0, c3=0; + static int in=0, out=0; + + in += nSamples; + nout = 0; + for (i = 0; i < nSamples; i++) { + c3 = cSamples[i]; + if (dindex < 1 || dindex >= 2.4) + QuiskPrintf ("dindex %.5f fdecim %.8f\n", dindex, fdecim); + if (dindex < 2) { +#if 0 + cSamples[nout++] = (1 - (dindex - 1)) * c1 + (dindex - 1) * c2; +#else + xm0 = dindex - 0; + xm1 = dindex - 1; + xm2 = dindex - 2; + xm3 = dindex - 3; + cSamples[nout++] = + (xm1 * xm2 * xm3 * c0 / -6.0 + xm0 * xm2 * xm3 * c1 / 2.0 + + xm0 * xm1 * xm3 * c2 / -2.0 + xm0 * xm1 * xm2 * c3 / 6.0); +#endif + out++; + dindex += fdecim - 1; + c0 = c1; + c1 = c2; + c2 = c3; + } + else { + if (dindex > 2.5) QuiskPrintf ("Skip at %.2f\n", dindex); + c0 = c1; + c1 = c2; + c2 = c3; + dindex -= 1; + } + } + //QuiskPrintf ("in %d out %d\n", in, out); + return nout; +} + +// Create an fftw plan; attempt to use loaded wisdom cache or update existing cache. +static fftw_plan quisk_create_or_cache_fftw_plan_dft_1d(int fft_size, fftw_complex *in, fftw_complex *out, int sign, unsigned flags) +{ + fftw_plan plan = fftw_plan_dft_1d(fft_size, in, out, sign, flags | FFTW_WISDOM_ONLY); + if(!plan) { + // Nothing in the wisdom file for this config; create and save new wisdom + plan = fftw_plan_dft_1d(fft_size, in, out, sign, flags); + fftw_export_wisdom_to_filename(fftw_wisdom_name); + } + return plan; +} + +#define QUISK_NB_HWINDOW_SECS 500.E-6 // half-size of blanking window in seconds +static void NoiseBlanker(complex double * cSamples, int nSamples) +{ + static complex double * cSaved = NULL; + static double * dSaved = NULL; + static double save_sum; + static int save_size, hwindow_size, state, index, win_index; + static int sample_rate = -1; + int i, j, k, is_pulse; + double mag, limit; + complex double samp; +#if DEBUG + static time_t time0 = 0; + static int debug_count = 0; +#endif + + if (quisk_noise_blanker <= 0) + return; + if (quisk_sound_state.sample_rate != sample_rate) { // Initialization + sample_rate = quisk_sound_state.sample_rate; + state = 0; + index = 0; + win_index = 0; + save_sum = 0.0; + hwindow_size = (int)(sample_rate * QUISK_NB_HWINDOW_SECS + 0.5); + save_size = hwindow_size * 3; // number of samples in the average + i = save_size * sizeof(double); + dSaved = (double *) realloc(dSaved, i); + memset (dSaved, 0, i); + i = save_size * sizeof(complex double); + cSaved = (complex double *)realloc(cSaved, i); + memset (cSaved, 0, i); +#if DEBUG + QuiskPrintf ("Noise blanker: save_size %d hwindow_size %d\n", + save_size, hwindow_size); +#endif + } + switch(quisk_noise_blanker) { + case 1: + default: + limit = 6.0; + break; + case 2: + limit = 4.0; + break; + case 3: + limit = 2.5; + break; + } + for (i = 0; i < nSamples; i++) { + // output oldest sample, save newest + samp = cSamples[i]; // newest sample + cSamples[i] = cSaved[index]; // oldest sample + cSaved[index] = samp; + // use newest sample + mag = cabs(samp); + save_sum -= dSaved[index]; // remove oldest sample magnitude + dSaved[index] = mag; // save newest sample magnitude + save_sum += mag; // update sum of samples + if (mag <= save_sum / save_size * limit) // see if we have a large pulse + is_pulse = 0; + else + is_pulse = 1; + switch (state) { + case 0: // Normal state + if (is_pulse) { // wait for a pulse + state = 1; + k = index; + for (j = 0; j < hwindow_size; j++) { // apply window to prior samples + cSaved[k--] *= (double)j / hwindow_size; + if (k < 0) + k = save_size - 1; + } + } + else if (win_index) { // pulses have stopped, increase window to 1.0 + cSaved[index] *= (double)win_index / hwindow_size; + if (++win_index >= hwindow_size) + win_index = 0; // no more window + } + break; + case 1: // we got a pulse + cSaved[index] = 0; // zero samples until the pulses stop + if ( ! is_pulse) { + // start raising the window, but be prepared to window another pulse + state = 0; + win_index = 1; + } + break; + } +#if DEBUG + if (debug_count) { + QuiskPrintf ("%d", is_pulse); + if (--debug_count == 0) + QuiskPrintf ("\n"); + } + else if (is_pulse && time(NULL) != time0) { + time0 = time(NULL); + debug_count = hwindow_size * 2; + QuiskPrintf ("%d", is_pulse); + } +#endif + if (++index >= save_size) + index = 0; + } + return; +} + +#define NOTCH_DEBUG 0 +#define NOTCH_DATA_SIZE 2048 +#define NOTCH_FILTER_DESIGN_SIZE NOTCH_DATA_SIZE / 4 +#define NOTCH_FILTER_SIZE (NOTCH_FILTER_DESIGN_SIZE - 1) +#define NOTCH_FILTER_FFT_SIZE (NOTCH_FILTER_SIZE / 2 + 1) +#define NOTCH_DATA_START_SIZE (NOTCH_FILTER_SIZE - 1) +#define NOTCH_DATA_OUTPUT_SIZE (NOTCH_DATA_SIZE - NOTCH_DATA_START_SIZE) +#define NOTCH_FFT_SIZE (NOTCH_DATA_SIZE / 2 + 1) +static void dAutoNotch(double * dsamples, int nSamples, int sidetone, int rate) +{ + int i, j, k, i1, i2, inp, signal, delta_sig, delta_i1, half_width; + double d, d1, d2, avg; + static int old1, count1, old2, count2; + static int index; + static fftw_plan planFwd=NULL; + static fftw_plan planRev,fltrFwd, fltrRev; + static double data_in[NOTCH_DATA_SIZE]; + static double data_out[NOTCH_DATA_SIZE]; + static complex double notch_fft[NOTCH_FFT_SIZE]; + static double fft_window[NOTCH_DATA_SIZE]; + static double fltr_in[NOTCH_DATA_SIZE]; + static double fltr_out[NOTCH_FILTER_DESIGN_SIZE]; + static complex double fltr_fft[NOTCH_FFT_SIZE]; + static double average_fft[NOTCH_FFT_SIZE]; + static int fltrSig; +#if NOTCH_DEBUG + static char * txt; + double dmax; +#endif + + if ( ! planFwd) { // set up FFT plans + planFwd = fftw_plan_dft_r2c_1d(NOTCH_DATA_SIZE, data_in, notch_fft, FFTW_MEASURE); + planRev = fftw_plan_dft_c2r_1d(NOTCH_DATA_SIZE, notch_fft, data_out, FFTW_MEASURE); // destroys notch_fft + fltrFwd = fftw_plan_dft_r2c_1d(NOTCH_DATA_SIZE, fltr_in, fltr_fft, FFTW_MEASURE); + fltrRev = fftw_plan_dft_c2r_1d(NOTCH_FILTER_DESIGN_SIZE, fltr_fft, fltr_out, FFTW_MEASURE); + for (i = 0; i < NOTCH_FILTER_SIZE; i++) + fft_window[i] = 0.50 - 0.50 * cos(2. * M_PI * i / (NOTCH_FILTER_SIZE)); // Hanning + //fft_window[i] = 0.54 - 0.46 * cos(2. * M_PI * i / (NOTCH_FILTER_SIZE)); // Hamming + } + if ( ! dsamples) { // initialize + index = NOTCH_DATA_START_SIZE; + fltrSig = -1; + old1 = old2 = 0; + count1 = count2 = -4; + memset(data_out, 0, sizeof(double) * NOTCH_DATA_SIZE); + memset(data_in, 0, sizeof(double) * NOTCH_DATA_SIZE); + memset(average_fft, 0, sizeof(double) * NOTCH_FFT_SIZE); + return; + } + if ( ! quisk_auto_notch) + return; + // index into FFT data = frequency * 2 * NOTCH_FFT_SIZE / rate + // index into filter design = frequency * 2 * NOTCH_FILTER_FFT_SIZE / rate + for (inp = 0; inp < nSamples; inp++) { + data_in[index] = dsamples[inp]; + dsamples[inp] = data_out[index]; + if (++index >= NOTCH_DATA_SIZE) { // we have a full FFT of samples + index = NOTCH_DATA_START_SIZE; + fftw_execute(planFwd); // Calculate forward FFT + // Find maximum FFT bins + delta_sig = (300 * 2 * NOTCH_FFT_SIZE + rate / 2) / rate; // small frequency interval + delta_i1 = (400 * 2 * NOTCH_FFT_SIZE + rate / 2) / rate; // small frequency interval + if (sidetone != 0) // For CW, accept a signal at the frequency of the RIT + signal = (abs(sidetone) * 2 * NOTCH_FFT_SIZE + rate / 2) / rate; + else + signal = -999; + avg = 1; +#if NOTCH_DEBUG + dmax = 0; +#endif + d1 = 0; + i1 = 0; // First maximum signal + for (i = 0; i < NOTCH_FFT_SIZE; i++) { + d = cabs(notch_fft[i]); + avg += d; + //average_fft[i] = 0.9 * average_fft[i] + 0.1 * d; + average_fft[i] = 0.5 * average_fft[i] + 0.5 * d; + if (abs(i - signal) > delta_sig && average_fft[i] > d1) { + d1 = average_fft[i]; + i1 = i; +#if NOTCH_DEBUG + dmax = d; +#endif + } + } + if (abs(i1 - old1) < 3) // See if the maximum bin i1 is changing + count1++; + else + count1--; + if (count1 > 4) + count1 = 4; + else if (count1 < -1) + count1 = -1; + if (count1 < 0) + old1 = i1; + avg /= NOTCH_FFT_SIZE; + d2 = 0; + i2 = 0; // Next maximum signal not near the first + for (i = 0; i < NOTCH_FFT_SIZE; i++) { + if (abs(i - signal) > delta_sig && abs(i - i1) > delta_i1 && average_fft[i] > d2) { + d2 = average_fft[i]; + i2 = i; + } + } + if (abs(i2 - old2) < 3) // See if the maximum bin i2 is changing + count2++; + else + count2--; + if (count2 > 4) + count2 = 4; + else if (count2 < -2) + count2 = -2; + if (count2 < 0) + old2 = i2; + + if (count1 > 0 && count2 > 0) + k = i1 + 10000 * i2; // trial filter index + else if(count1 > 0) + k = i1; + else + k = 0; + // Make the filter if it is different + if (fltrSig != k) { + fltrSig = k; + half_width = (100 * 2 * NOTCH_FILTER_FFT_SIZE + rate / 2) / rate; // half the width of the notch + if (half_width < 3) + half_width = 3; + for (i = 0; i < NOTCH_FILTER_FFT_SIZE; i++) + fltr_fft[i] = 1.0; + k = (i1 + 2) / 4; // Ratio of index values is 4 +#if NOTCH_DEBUG + txt = "Fxx"; +#endif + if (count1 > 0) { +#if NOTCH_DEBUG + txt = "F1"; +#endif + for (i = -half_width; i <= half_width; i++) { + j = k + i; + if (j >= 0 && j < NOTCH_FILTER_FFT_SIZE) + fltr_fft[j] = 0.0; + } + } + k = (i2 + 2) / 4; // Ratio of index values is 4 + if (count1 > 0 && count2 > 0) { +#if NOTCH_DEBUG + txt = "F12"; +#endif + for (i = -half_width; i <= half_width; i++) { + j = k + i; + if (j >= 0 && j < NOTCH_FILTER_FFT_SIZE) + fltr_fft[j] = 0.0; + } + } + fftw_execute(fltrRev); + // center the coefficient zero, make the filter symetric, reduce the size by one + memmove(fltr_out + NOTCH_FILTER_DESIGN_SIZE / 2 - 1, fltr_out, sizeof(double) * (NOTCH_FILTER_SIZE / 2 - 1)); + for (i = NOTCH_FILTER_DESIGN_SIZE / 2 - 2, j = NOTCH_FILTER_DESIGN_SIZE / 2; i >= 0; i--, j++) + fltr_out[i] = fltr_out[j]; + for (i = 0; i < NOTCH_FILTER_SIZE; i++) + fltr_in[i] = fltr_out[i] * fft_window[i] / NOTCH_FILTER_DESIGN_SIZE; + for (i = NOTCH_FILTER_SIZE; i < NOTCH_DATA_SIZE; i++) + fltr_in[i] = 0.0; + fftw_execute(fltrFwd); // The filter is fltr_fft[] + } +#if NOTCH_DEBUG + QuiskPrintf("Max %12.0lf frequency index1 %3d %5d %12.0lf index2 %3d %5d %12.0lf avg %12.0lf %s\n", dmax, count1, i1, d1, count2, i2, d2, avg, txt); +#endif + for (i = 0; i < NOTCH_FFT_SIZE; i++) // Apply the filter + notch_fft[i] *= fltr_fft[i]; + fftw_execute(planRev); // Calculate inverse FFT + memmove(data_in, data_in + NOTCH_DATA_OUTPUT_SIZE, NOTCH_DATA_START_SIZE * sizeof(double)); + for (i = NOTCH_DATA_START_SIZE; i < NOTCH_DATA_SIZE; i++) + data_out[i] /= NOTCH_DATA_SIZE / 20; // Empirical + } + } + return; +} + +static int audio_fft_ready=0; +static double * audio_average_fft; +void quisk_calc_audio_graph(double scale, complex double * csamples, double * dsamples, int nSamples, int real) +{ // Calculate an FFT for the audio data. Samples are either csamples or dsamples; the other is NULL. + // The "scale" is the 0 dB reference. If "real", use the real part of csamples. + int i, k, inp; + static int index; + static int count_fft; + static int audio_fft_size; + static int audio_fft_count; + static fftw_plan plan = NULL; + static double * fft_window; + static complex double * audio_fft; + + if ( ! plan) { // malloc new space and initialize + index = 0; + count_fft = 0; + audio_fft_size = data_width; + //audio_fft_count = 48000 / audio_fft_size / 5; // Display refresh rate. + audio_fft_count = 8000 / audio_fft_size / 5; // Display refresh rate. + if (audio_fft_count <= 0) + audio_fft_count = 1; + fft_window = (double *)malloc(audio_fft_size * sizeof(double)); + audio_average_fft = (double *)malloc(audio_fft_size * sizeof(double)); + audio_fft = (complex double *)malloc(audio_fft_size * sizeof(complex double)); + plan = fftw_plan_dft_1d(audio_fft_size, audio_fft, audio_fft, FFTW_FORWARD, FFTW_MEASURE); + for (i = 0; i < audio_fft_size; i++) { + audio_average_fft[i] = 0; + fft_window[i] = 0.50 - 0.50 * cos(2. * M_PI * i / audio_fft_size); // Hanning window loss 50% + } + return; + } + if (audio_fft_ready == 0) { // calculate a new audio FFT + if (dsamples || real) // Lyons 2Ed p61 + scale *= audio_fft_size / 2.0; + else + scale *= audio_fft_size; + scale *= audio_fft_count; + scale *= 0.5; // correct for Hanning window loss + for (inp = 0; inp < nSamples; inp++) { + if (dsamples) + audio_fft[index] = dsamples[inp] / scale; + else if (real) + audio_fft[index] = creal(csamples[inp]) / scale; + else + audio_fft[index] = csamples[inp] / scale; + if (++index >= audio_fft_size) { // we have a full FFT of samples + index = 0; + for (i = 0; i < audio_fft_size; i++) + audio_fft[i] *= fft_window[i]; // multiply by window + fftw_execute(plan); // Calculate forward FFT + count_fft++; + k = 0; + for (i = audio_fft_size / 2; i < audio_fft_size; i++) // Negative frequencies + audio_average_fft[k++] += cabs(audio_fft[i]); + for (i = 0; i < audio_fft_size / 2; i++) // Positive frequencies + audio_average_fft[k++] += cabs(audio_fft[i]); + if (count_fft >= audio_fft_count) { + audio_fft_ready = 1; + count_fft = 0; + } + } + } + } +} + +static PyObject * get_audio_graph(PyObject * self, PyObject * args) +{ + int i; + double d2; + PyObject * tuple2; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + + if ( ! audio_fft_ready) { // a new graph is not yet available + Py_INCREF (Py_None); + return Py_None; + } + tuple2 = PyTuple_New(data_width); + for (i = 0; i < data_width; i++) { + d2 = audio_average_fft[i]; + if (d2 < 1E-10) + d2 = 1E-10; + d2 = 20.0 * log10(d2); + PyTuple_SetItem(tuple2, i, PyFloat_FromDouble(d2)); + audio_average_fft[i] = 0; + } + audio_fft_ready = 0; + return tuple2; +} + +static void d_delay(double * dsamples, int nSamples, int bank, int samp_delay) +{ // delay line (FIFO) to delay dsamples by samp_delay samples + int i; + double sample; + static struct { + double * buffer; + int index; + int buf_size; + } delay[MAX_RX_CHANNELS] = {{NULL, 0, 0}}; + + if ( ! delay[0].buffer) + for (i = 1; i < MAX_RX_CHANNELS; i++) + delay[i].buffer = NULL; + if ( ! delay[bank].buffer) { + delay[bank].buffer = (double *)malloc(samp_delay * sizeof(double)); + delay[bank].index = 0; + delay[bank].buf_size = samp_delay; + for (i = 0; i < samp_delay; i++) + delay[bank].buffer[i] = 0; + } + for (i = 0; i < nSamples; i++) { + sample = delay[bank].buffer[delay[bank].index]; + delay[bank].buffer[delay[bank].index] = dsamples[i]; + dsamples[i] = sample; + if (++delay[bank].index >= delay[bank].buf_size) + delay[bank].index = 0; + } +} + +static void ssb_squelch(double * dsamples, int nSamples, int samp_rate, struct _MeasureSquelch * MS) +{ + int i, bw, bw1, bw2, inp; + double d, arith_avg, geom_avg, ratio; + complex double c; + static fftw_plan plan = NULL; + static double * fft_window; + static complex double * out_fft; +#ifdef QUISK_PRINT_LEVELS + static int timer = 0; + timer += nSamples; +#endif + + if ( ! MS->in_fft) { + MS->in_fft = (double *)fftw_malloc(SQUELCH_FFT_SIZE * sizeof(double)); + MS->index = 0; + MS->sq_open = 0; + } + if ( ! plan) { // malloc new space and initialize + fft_window = (double *)malloc(SQUELCH_FFT_SIZE * sizeof(double)); + out_fft = (complex double *)fftw_malloc((SQUELCH_FFT_SIZE / 2 + 1) * sizeof(complex double)); + // out_fft[0] is DC, then positive frequencies, then out_fft[N/2] is Nyquist. + plan = fftw_plan_dft_r2c_1d(SQUELCH_FFT_SIZE, MS->in_fft, out_fft, FFTW_MEASURE); + for (i = 0; i < SQUELCH_FFT_SIZE; i++) + fft_window[i] = 0.50 - 0.50 * cos(2. * M_PI * i / SQUELCH_FFT_SIZE); // Hanning window + return; + } + for (inp = 0; inp < nSamples; inp++) { + MS->in_fft[MS->index++] = dsamples[inp]; + if (MS->index >= SQUELCH_FFT_SIZE) { // we have a full FFT of samples + MS->index = 0; + for (i = 0; i < SQUELCH_FFT_SIZE; i++) + MS->in_fft[i] *= fft_window[i]; // multiply by window + fftw_execute_dft_r2c(plan, MS->in_fft, out_fft); // Calculate forward FFT + bw = filter_bandwidth[0]; // Calculate the FFT bins within the filter bandwidth + if (bw > 3000) + bw = 3000; + bw1 = 300 * SQUELCH_FFT_SIZE / samp_rate; // start 300 Hz + bw2 = (bw + 300) * SQUELCH_FFT_SIZE / samp_rate; // end 300 Hz + bw + arith_avg = 0.0; + geom_avg = 0.0; + for (i = bw1; i < bw2; i++) { + c = out_fft[i] / CLIP16; + d = creal(c) * creal(c) + cimag(c) * cimag(c); + if (d > 1E-4) { + arith_avg += d; + geom_avg += log(d); + } + } +#ifdef QUISK_PRINT_SPECTRUM + // Combine FFT into spectral bands + int j; + for (i = 0; i < 128; i += 16) { + d = 0; + for (j = i; j < i + 16; j++) { + c = out_fft[i] / CLIP16; + d += creal(c) * creal(c) + cimag(c) * cimag(c); + } + d = log(d / 16); + QuiskPrintf ("%12.3f", d); + if (i == 112) + QuiskPrintf("\n"); + } +#endif + if (arith_avg > 1E-4) { + bw = bw2 - bw1; + arith_avg = log(arith_avg / bw); + geom_avg /= bw; + ratio = arith_avg - geom_avg; + } + else { + ratio = 1.0; + } + // For band noise, ratio is 0.57 + if (ratio > ssb_squelch_level * 0.005) + MS->sq_open = samp_rate; // one second timer +#ifdef QUISK_PRINT_LEVELS + if (timer >= samp_rate / 2) { + timer = 0; + QuiskPrintf ("squelch %6d A %6.3f G %6.3f A-G %6.3f\n", + MS->sq_open, arith_avg, geom_avg, ratio); +#ifdef QUISK_PRINT_SPECTRUM + for (i = 0; i < 128; i += 16) + QuiskPrintf ("%5d - %4d", i * samp_rate / SQUELCH_FFT_SIZE, (i + 16) * samp_rate / SQUELCH_FFT_SIZE); + QuiskPrintf ("\n"); +#endif + } +#endif + } + } + MS->sq_open -= nSamples; + if (MS->sq_open < 0) + MS->sq_open = 0; + MS->squelch_active = MS->sq_open == 0; +} + +static complex double dRxFilterOut(complex double sample, int bank, int nFilter) +{ // Rx FIR filter; bank is the static storage index, and must be different for different data streams. + // Multiple filters are at nFilter. + complex double cx; + int j, k; + static int init = 0; + static struct stStorage { + int indexFilter; // current index into sample buffer + complex double bufFilterC[MAX_FILTER_SIZE]; // Digital filter sample buffer + } Storage[MAX_RX_CHANNELS]; + struct stStorage * ptBuf = Storage + bank; + double * filtI; + + if ( ! init) { + init = 1; + for (j = 0; j < MAX_RX_CHANNELS; j++) + memset(Storage + j, 0, sizeof(struct stStorage)); + } + + if ( ! sizeFilter) + return sample; + if (ptBuf->indexFilter >= sizeFilter) + ptBuf->indexFilter = 0; + ptBuf->bufFilterC[ptBuf->indexFilter] = sample; + cx = 0; + filtI = cFilterI[nFilter]; + j = ptBuf->indexFilter; + for (k = 0; k < sizeFilter; k++) { + cx += ptBuf->bufFilterC[j] * filtI[k]; + if (++j >= sizeFilter) + j = 0; + } + ptBuf->indexFilter++; + return cx; +} + +complex double cRxFilterOut(complex double sample, int bank, int nFilter) +{ // Rx FIR filter; bank is the static storage index, and must be different for different data streams. + // Multiple filters are at nFilter. + double accI, accQ; + double * filtI, * filtQ; + int j, k; + static int init = 0; + static struct stStorage { + int indexFilter; // current index into sample buffer + double bufFilterI[MAX_FILTER_SIZE]; // Digital filter sample buffer + double bufFilterQ[MAX_FILTER_SIZE]; // Digital filter sample buffer + } Storage[MAX_RX_CHANNELS]; + struct stStorage * ptBuf = Storage + bank; + + if ( ! init) { + init = 1; + for (j = 0; j < MAX_RX_CHANNELS; j++) + memset(Storage + j, 0, sizeof(struct stStorage)); + } + + if ( ! sizeFilter) + return sample; + if (ptBuf->indexFilter >= sizeFilter) + ptBuf->indexFilter = 0; + ptBuf->bufFilterI[ptBuf->indexFilter] = creal(sample); + ptBuf->bufFilterQ[ptBuf->indexFilter] = cimag(sample); + filtI = cFilterI[nFilter]; + filtQ = cFilterQ[nFilter]; + accI = accQ = 0; + j = ptBuf->indexFilter; + for (k = 0; k < sizeFilter; k++) { + accI += ptBuf->bufFilterI[j] * filtI[k]; + accQ += ptBuf->bufFilterQ[j] * filtQ[k]; + if (++j >= sizeFilter) + j = 0; + } + ptBuf->indexFilter++; + return accI + I * accQ; +} + +static void AddTestTone(complex double * cSamples, int nSamples) +{ + int i; + //int freq; + //static int old_freq=0; + static complex double testtoneVector = 21474836.47; // -40 dB + static complex double audioVector = 1.0; + complex double audioPhase; + + switch (rxMode) { + default: + //testtonePhase = cexp(I * 2 * M_PI * (quisk_sidetoneCtrl - 500) / 1000.0); + for (i = 0; i < nSamples; i++) { + cSamples[i] += testtoneVector; + testtoneVector *= testtonePhase; + } + break; + case AM: // AM + //audioPhase = cexp(I * 2 * M_PI * quisk_sidetoneCtrl * 5 / sample_rate); + audioPhase = cexp(I * 2.0 * M_PI * 1000 / quisk_sound_state.sample_rate); + for (i = 0; i < nSamples; i++) { + cSamples[i] += testtoneVector * (1.0 + creal(audioVector)); + testtoneVector *= testtonePhase; + audioVector *= audioPhase; + } + break; + case FM: // FM + case DGT_FM: +#if 0 + freq = quisk_sidetoneCtrl * 5; + audioPhase = cexp(I * 2 * M_PI * freq / quisk_sound_state.sample_rate); + if (old_freq != freq) { + old_freq = freq; + printf("test tone frequency %d Hz\n", freq); + } +#else + audioPhase = cexp(I * 2.0 * M_PI * 1000 / quisk_sound_state.sample_rate); +#endif + for (i = 0; i < nSamples; i++) { + cSamples[i] += testtoneVector * cexp(I * creal(audioVector)); + testtoneVector *= testtonePhase; + audioVector *= audioPhase; + } + break; + } +} + +static int IsSquelch(int freq) +{ // measure the signal level for squelch + int i, i1, i2, iBandwidth; + double meter; + + // This uses current_graph with width data_width + iBandwidth = 5000 * data_width / fft_sample_rate; // bandwidth determines number of pixels to average + if (iBandwidth < 1) + iBandwidth = 1; + i1 = (int)((double)freq * data_width / fft_sample_rate + data_width / 2.0 - iBandwidth / 2.0 + 0.5); + i2 = i1 + iBandwidth; + meter = 0; + if (i1 >= 0 && i2 < data_width) { // too close to edge? + for (i = i1; i < i2; i++) + meter += current_graph[i]; + } + meter /= iBandwidth; + if (meter == 0 || meter < squelch_level) + return 1; // meter == 0 means Rx freq is off-screen so squelch is on + else + return 0; +} + +static PyObject * set_record_state(PyObject * self, PyObject * args) +{ // called when a Record or Play button is pressed, or with -1 to poll + int button; + + if (!PyArg_ParseTuple (args, "i", &button)) + return NULL; + if ( ! quisk_record_buffer) { // initialize + quisk_record_bufsize = (int)(QuiskGetConfigDouble("max_record_minutes", 0.25) * quisk_sound_state.playback_rate * 60.0 + 0.2); + quisk_record_buffer = (float *)malloc(sizeof(float) * quisk_record_bufsize); + } + switch (button) { + case 0: // press record radio + case 4: // press record microphone + quisk_record_index = 0; + quisk_play_index = 0; + quisk_mic_index = 0; + quisk_record_full = 0; + if (button == 0) + quisk_record_state = TMP_RECORD_SPEAKERS; + else + quisk_record_state = TMP_RECORD_MIC; + break; + case 1: // release record + quisk_record_state = IDLE; + break; + case 2: // press play + if (quisk_record_full) { + quisk_play_index = quisk_record_index + 1; + if (quisk_play_index >= quisk_record_bufsize) + quisk_play_index = 0; + } + else { + quisk_play_index = 0; + } + quisk_mic_index = quisk_play_index; + quisk_record_state = TMP_PLAY_SPKR_MIC; + break; + case 3: // release play + quisk_record_state = IDLE; + quisk_close_file_play = 1; + break; + case 5: // press play file + if (wavFpSound) + fseek (wavFpSound, wavStart, SEEK_SET); + if (wavFpMic) + fseek (wavFpMic, wavStart, SEEK_SET); + quisk_record_state = FILE_PLAY_SPKR_MIC; + break; + case 6: // press play samples file + if (wavFpSound) + fseek (wavFpSound, wavStart, SEEK_SET); + quisk_record_state = FILE_PLAY_SAMPLES; + break; + } + return PyInt_FromLong(quisk_record_state != TMP_PLAY_SPKR_MIC && quisk_record_state != FILE_PLAY_SPKR_MIC && quisk_record_state != FILE_PLAY_SAMPLES); +} + +void quisk_tmp_record(complex double * cSamples, int nSamples, double scale) // save sound +{ + int i; + + for (i = 0; i < nSamples; i++) { + quisk_record_buffer[quisk_record_index++] = creal(cSamples[i]) * scale; + if (quisk_record_index >= quisk_record_bufsize) { + quisk_record_index = 0; + quisk_record_full = 1; + } + } +} + +void quisk_tmp_playback(complex double * cSamples, int nSamples, double volume) +{ // replace radio sound with saved sound + int i; + double d; + + for (i = 0; i < nSamples; i++) { + d = quisk_record_buffer[quisk_play_index++] * volume; + cSamples[i] = d + I * d; + if (quisk_play_index >= quisk_record_bufsize) + quisk_play_index = 0; + if (quisk_play_index == quisk_record_index) { + quisk_record_state = IDLE; + return; + } + } +} + +static PyObject * tmp_record_save(PyObject * self, PyObject * args) +{ + const char * fname; + int i, start; + complex double * ptC; + struct wav_file file_rec_tmp; + + if (!PyArg_ParseTuple (args, "s", &fname)) + return NULL; + memset(&file_rec_tmp, 0, sizeof(struct wav_file)); + strMcpy(file_rec_tmp.file_name, fname, QUISK_PATH_SIZE); + quisk_record_audio(&file_rec_tmp, NULL, -1); // Open file + if ( ! file_rec_tmp.fp) { + QuiskPrintf("Failed to open file %s\n", fname); + } + else { + if (quisk_record_full) { + start = quisk_record_index + 1; + if (start >= quisk_record_bufsize) + start = 0; + } + else { + start = 0; + } + ptC = malloc(quisk_record_bufsize * sizeof(complex double)); + for (i = 0; i < quisk_record_bufsize; i++) + ptC[i] = quisk_record_buffer[i]; + if (start > 0) + quisk_record_audio(&file_rec_tmp, ptC + start, quisk_record_bufsize - start); + quisk_record_audio(&file_rec_tmp, ptC, quisk_record_index); + free(ptC); + quisk_record_audio(&file_rec_tmp, NULL, -2); // Close file + } + Py_INCREF (Py_None); + return Py_None; +} + +void quisk_tmp_microphone(complex double * cSamples, int nSamples) +{ // replace microphone samples with saved sound + int i; + double d; + + for (i = 0; i < nSamples; i++) { + d = quisk_record_buffer[quisk_mic_index++]; + cSamples[i] = d + I * d; + if (quisk_mic_index >= quisk_record_bufsize) + quisk_mic_index = 0; + if (quisk_mic_index == quisk_record_index) { + quisk_record_state = IDLE; + return; + } + } +} + +static void wav_files_close(void) +{ + if (wavFpMic) + fclose(wavFpMic); + if (wavFpSound) + fclose(wavFpSound); + wavFpSound = wavFpMic = NULL; +} + +static PyObject * open_wav_file_play(PyObject * self, PyObject * args) +{ +// Open the same file twice and record the start of the sound data. +// One will be used to replace the speaker sound, the other replaces the mic sound. +// Use only one for the I/Q samples. +// The WAV file must be recorded at 48000 Hertz in S16_LE format monophonic for audio files. +// The WAV file must be recorded at the sample_rate in IEEE format stereo for the I/Q samples file. + const char * fname; + char name[5]; + int size, rate=0; + + if (!PyArg_ParseTuple (args, "s", &fname)) + return NULL; + wav_files_close(); + wavFpSound = fopen(fname, "rb"); + if (!wavFpSound) { + QuiskPrintf("open wav file failed\n"); + return PyInt_FromLong(-1); + } + wavStart = 0; + while (1) { + if (fread (name, 4, 1, wavFpSound) != 1) + break; + if (fread (&size, 4, 1, wavFpSound) != 1) + break; + name[4] = 0; + // QuiskPrintf("name %s size %d\n", name, size); + if (!strncmp(name, "RIFF", 4)) + fseek (wavFpSound, 4, SEEK_CUR); // Skip "WAVE" + else if (!strncmp(name, "fmt ", 4)) { // format data starts here + if (fread (&rate, 4, 1, wavFpSound) != 1) // skip these fields + break; + if (fread (&rate, 4, 1, wavFpSound) != 1) // sample rate + break; + //QuiskPrintf ("rate %d\n", rate); + fseek (wavFpSound, size - 8, SEEK_CUR); // skip remainder + } + else if (!strncmp(name, "data", 4)) { // sound data starts here + wavStart = ftell(wavFpSound); + break; + } + else // Skip other records + fseek (wavFpSound, size, SEEK_CUR); + } + if (!wavStart) { // Failure to find "data" record + fclose(wavFpSound); + wavFpSound = NULL; + QuiskPrintf("open wav failed to find the data chunk\n"); + return PyInt_FromLong(-2); + } + wavFpMic = fopen(fname, "rb"); + if (!wavFpMic) { + QuiskPrintf("open microphone wav file failed\n"); + fclose(wavFpSound); + wavFpSound = NULL; + return PyInt_FromLong(-4); + } + return PyInt_FromLong(rate); +} + +void quisk_file_playback(complex double * cSamples, int nSamples, double volume) +{ + // Replace radio sound by file samples. + // The sample rate must equal quisk_sound_state.mic_sample_rate. + int i; + short sh; + double d; + + if (wavFpSound) { + for (i = 0; i < nSamples; i++) { + if (fread(&sh, 2, 1, wavFpSound) != 1) { + quisk_record_state = IDLE; + break; + } + d = sh * ((double)CLIP32 / CLIP16) * volume; + cSamples[i] = d + I * d; + } + } +} + +void quisk_play_samples(complex double * cSamples, int nSamples) +{ + int i; + float fre, fim; + + if (wavFpSound) { + for (i = 0; i < nSamples; i++) { + if (fread(&fre, 4, 1, wavFpSound) != 1 || fread(&fim, 4, 1, wavFpSound) != 1) { + quisk_record_state = IDLE; + break; + } + fre *= CLIP32; + fim *= CLIP32; + cSamples[i] = fre + I * fim; + } + } +} + +#define BUF2CHAN_SIZE 12000 +static int Buffer2Chan(double * samp1, int count1, double * samp2, int count2) +{ // return the minimum of count1 and count2, buffering as necessary + int nout; + static int nbuf1=0, nbuf2=0; + static double buf1[BUF2CHAN_SIZE], buf2[BUF2CHAN_SIZE]; + + if (samp1 == NULL) { // initialize + nbuf1 = nbuf2 = 0; + return 0; + } + if (nbuf1 == 0 && nbuf2 == 0 && count1 == count2) // nothing to do + return count1; + if (count1 + nbuf1 >= BUF2CHAN_SIZE || count2 + nbuf2 >= BUF2CHAN_SIZE) { // overflow + if (DEBUG || DEBUG_IO) + QuiskPrintf("Overflow in Buffer2Chan nbuf1 %d nbuf2 %d size %d\n", nbuf1, nbuf2, BUF2CHAN_SIZE); + nbuf1 = nbuf2 = 0; + } + memcpy(buf1 + nbuf1, samp1, count1 * sizeof(double)); // add samples to buffer + nbuf1 += count1; + memcpy(buf2 + nbuf2, samp2, count2 * sizeof(double)); + nbuf2 += count2; + if (nbuf1 <= nbuf2) + nout = nbuf1; // number of samples to output + else + nout = nbuf2; + //if (count1 + nbuf1 >= 2000 || count2 + nbuf2 >= 2000) + // QuiskPrintf("Buffer2Chan nbuf1 %d nbuf2 %d nout %d\n", nbuf1, nbuf2, nout); + memcpy(samp1, buf1, nout * sizeof(double)); // output samples + nbuf1 -= nout; + memmove(buf1, buf1 + nout, nbuf1 * sizeof(double)); + memcpy(samp2, buf2, nout * sizeof(double)); + nbuf2 -= nout; + memmove(buf2, buf2 + nout, nbuf2 * sizeof(double)); + return nout; +} + +void quisk_file_microphone(complex double * cSamples, int nSamples) +{ + // Replace mic samples by file samples. + // The sample rate must equal quisk_sound_state.mic_sample_rate. + int i; + short sh; + double d; + + if (wavFpMic) { + for (i = 0; i < nSamples; i++) { + if (fread(&sh, 2, 1, wavFpMic) != 1) { + quisk_record_state = IDLE; + break; + } + d = sh * ((double)CLIP32 / CLIP16); + cSamples[i] = d + I * d; + } + } +} + +int PlanDecimation(int * pt2, int * pt3, int * pt5) // search for a suitable decimation scheme +{ + int i, best, try, i2, i3, i5, decim2, decim3, decim5; + + best = quisk_sound_state.sample_rate; + decim2 = decim3 = decim5 = 0; + for (i2 = 0; i2 <= 6; i2++) { // limit to number of /2 filters, currently 6 + for (i3 = 0; i3 <= 3; i3++) { // limit to number of /3 filters, currently 3 + for (i5 = 0; i5 <= 3; i5++) { // limit to number of /5 filters, currently 3 + try = quisk_sound_state.sample_rate; + for (i = 0; i < i2; i++) + try /= 2; + for (i = 0; i < i3; i++) + try /= 3; + for (i = 0; i < i5; i++) + try /= 5; + if (try >= 48000 && try < best) { + decim2 = i2; + decim3 = i3; + decim5 = i5; + best = try; + } + } + } + } + if (best >= 50000) // special rate converter + best = best * 24 / 25; + if (DEBUG) + QuiskPrintf ("Plan Decimation: rate %i, best %i, decim2 %i, decim3 %i, decim5 %i\n", + quisk_sound_state.sample_rate, best, decim2, decim3, decim5); + if (best > 72000) + QuiskPrintf("Failure to plan a suitable decimation in quisk_process_decimate\n"); + if (pt2) { // return decimations + *pt2 = decim2; + *pt3 = decim3; + *pt5 = decim5; + } + return best; +} + +static int quisk_process_decimate(complex double * cSamples, int nSamples, int bank, rx_mode_type rx_mode) +{ // Changes here will require changes to get_filter_rate(); + int i, i2, i3, i5; + static int decim2, decim3, decim5; + static int old_rate = 0; + static struct stStorage { + struct quisk_cHB45Filter HalfBand1; + struct quisk_cHB45Filter HalfBand2; + struct quisk_cHB45Filter HalfBand3; + struct quisk_cHB45Filter HalfBand4; + struct quisk_cHB45Filter HalfBand5; + struct quisk_cFilter filtSdriq111; + struct quisk_cFilter filtSdriq53; + struct quisk_cFilter filtSdriq133; + struct quisk_cFilter filtSdriq167; + struct quisk_cFilter filtSdriq185; + struct quisk_cFilter filtDecim3; + struct quisk_cFilter filtDecim3B; + struct quisk_cFilter filtDecim3C; + struct quisk_cFilter filtDecim5; + struct quisk_cFilter filtDecim5B; + struct quisk_cFilter filtDecim5S; + struct quisk_cFilter filtDecim48to24; + struct quisk_cFilter filtI3D25; + struct quisk_cFilter filt300D5; + } Storage[MAX_RX_CHANNELS] ; + + if ( ! cSamples) { // Initialize all filters + for (i = 0; i < MAX_RX_CHANNELS; i++) { + memset(&Storage[i].HalfBand1, 0, sizeof(struct quisk_cHB45Filter)); + memset(&Storage[i].HalfBand2, 0, sizeof(struct quisk_cHB45Filter)); + memset(&Storage[i].HalfBand3, 0, sizeof(struct quisk_cHB45Filter)); + memset(&Storage[i].HalfBand4, 0, sizeof(struct quisk_cHB45Filter)); + memset(&Storage[i].HalfBand5, 0, sizeof(struct quisk_cHB45Filter)); + quisk_filt_cInit(&Storage[i].filtSdriq111, quiskFilt111D2Coefs, sizeof(quiskFilt111D2Coefs)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filtSdriq53, quiskFilt53D1Coefs, sizeof(quiskFilt53D1Coefs)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filtSdriq133, quiskFilt133D2Coefs, sizeof(quiskFilt133D2Coefs)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filtSdriq167, quiskFilt167D3Coefs, sizeof(quiskFilt167D3Coefs)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filtSdriq185, quiskFilt185D3Coefs, sizeof(quiskFilt185D3Coefs)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filtDecim3, quiskFilt144D3Coefs, sizeof(quiskFilt144D3Coefs)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filtDecim3B, quiskFilt144D3Coefs, sizeof(quiskFilt144D3Coefs)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filtDecim3C, quiskFilt144D3Coefs, sizeof(quiskFilt144D3Coefs)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filtDecim5, quiskFilt240D5CoefsSharp, sizeof(quiskFilt240D5CoefsSharp)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filtDecim5B, quiskFilt240D5CoefsSharp, sizeof(quiskFilt240D5CoefsSharp)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filtDecim5S, quiskFilt240D5CoefsSharp, sizeof(quiskFilt240D5CoefsSharp)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filtDecim48to24, quiskFilt48dec24Coefs, sizeof(quiskFilt48dec24Coefs)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filtI3D25, quiskFiltI3D25Coefs, sizeof(quiskFiltI3D25Coefs)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filt300D5, quiskFilt300D5Coefs, sizeof(quiskFilt300D5Coefs)/sizeof(double)); + } + return 0; + } + if (quisk_sound_state.sample_rate != old_rate) { + old_rate = quisk_sound_state.sample_rate; + PlanDecimation(&decim2, &decim3, &decim5); + } + // Decimate: Lower the sample rate to 48000 sps (or approx). Filters are designed for + // a pass bandwidth of 20 kHz and a stop bandwidth of 24 kHz. + // We use 48 ksps to accommodate wide digital modes. + switch((quisk_sound_state.sample_rate + 100) / 1000) { + case 41: + quisk_decim_srate = 48000; + break; + case 53: // SDR-IQ + quisk_decim_srate = quisk_sound_state.sample_rate; + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtSdriq53, 1); + break; + case 111: // SDR-IQ + quisk_decim_srate = quisk_sound_state.sample_rate / 2; + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtSdriq111, 2); + break; + case 133: // SDR-IQ + quisk_decim_srate = quisk_sound_state.sample_rate / 2; + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtSdriq133, 2); + break; + case 185: // SDR-IQ + quisk_decim_srate = quisk_sound_state.sample_rate / 3; + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtSdriq185, 3); + break; + case 370: + quisk_decim_srate = quisk_sound_state.sample_rate / 6; + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand2); + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtSdriq185, 3); + break; + case 740: + quisk_decim_srate = quisk_sound_state.sample_rate / 12; + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand2); + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand3); + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtSdriq185, 3); + break; + case 1333: + quisk_decim_srate = quisk_sound_state.sample_rate / 24; + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand1); + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand2); + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand3); + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtSdriq167, 3); + break; + default: + quisk_decim_srate = quisk_sound_state.sample_rate; + i2 = decim2; // decimate by 2 except for the final /2 filter + if (i2 > 1) { + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand1); + quisk_decim_srate /= 2; + i2--; + } + if (i2 > 1) { + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand2); + quisk_decim_srate /= 2; + i2--; + } + if (i2 > 1) { + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand3); + quisk_decim_srate /= 2; + i2--; + } + if (i2 > 1) { + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand4); + quisk_decim_srate /= 2; + i2--; + } + if (i2 > 1) { + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand5); + quisk_decim_srate /= 2; + i2--; + } + i3 = decim3; // decimate by 3 + if (i3 > 0) { + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim3, 3); + quisk_decim_srate /= 3; + i3--; + } + if (i3 > 0) { + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim3B, 3); + quisk_decim_srate /= 3; + i3--; + } + if (i3 > 0) { + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim3C, 3); + quisk_decim_srate /= 3; + i3--; + } + i5 = decim5; // decimate by 5 + if (i5 > 0) { + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim5, 5); + quisk_decim_srate /= 5; + i5--; + } + if (i5 > 0) { + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim5B, 5); + quisk_decim_srate /= 5; + i5--; + } + if (i5 > 0) { + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim5S, 5); + quisk_decim_srate /= 5; + i5--; + } + if (i2 > 0) { // decimate by 2 last - Unnecessary??? + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim48to24, 2); + quisk_decim_srate /= 2; + i2--; + } + if (quisk_decim_srate >= 50000) { + quisk_decim_srate = quisk_decim_srate * 24 / 25; + nSamples = quisk_cInterpDecim(cSamples, nSamples, &Storage[bank].filt300D5, 6, 5); // 60 kSps + nSamples = quisk_cInterpDecim(cSamples, nSamples, &Storage[bank].filtDecim5S, 4, 5); // 48 kSps + } + if (i2 != 0 || i3 != 0 || i5 != 0) + QuiskPrintf ("Failure in quisk.c in integer decimation for rate %d\n", quisk_sound_state.sample_rate); + if (DEBUG && quisk_decim_srate != 48000) + QuiskPrintf("Failure to achieve rate 48000. Rate is %i\n", quisk_decim_srate); + break; + } + return nSamples; +} + +static int quisk_process_demodulate(complex double * cSamples, double * dsamples, int nSamples, int bank, int nFilter, rx_mode_type rx_mode) +{ // Changes here will require changes to get_filter_rate(); + int i; + complex double cx; + double d, di, dd; + static struct AgcState Agc1 = {0.3, 16000, 0}, Agc2 = {0.3, 16000, 0}; +//static int count=0; +//static double phase=0; + static struct stStorage { + complex double fm_1; // Sample delayed by one + double dc_remove; // DC removal for AM + double FM_www; + double FM_nnn, FM_a_0, FM_a_1, FM_b_1, FM_x_1, FM_y_1; // filter for FM + //double FM_phase; + struct quisk_cHB45Filter HalfBand4; + struct quisk_cHB45Filter HalfBand5; + struct quisk_dHB45Filter HalfBand6; + struct quisk_dHB45Filter HalfBand7; + struct quisk_dFilter filtAudio48p3; + struct quisk_dFilter filtAudio24p3; + struct quisk_dFilter filtAudio24p4; + struct quisk_dFilter filtAudio12p2; + struct quisk_dFilter filtAudio24p6; + struct quisk_dFilter filtAudioFmHp; + struct quisk_cFilter filtDecim16to8; + struct quisk_cFilter filtDecim48to24; + struct quisk_cFilter filtDecim48to16; + //struct quisk_dFilter filtFMdiff; + } Storage[MAX_RX_CHANNELS] ; + + if ( ! cSamples) { // Initialize all filters + for (i = 0; i < MAX_RX_CHANNELS; i++) { + memset(&Storage[i].HalfBand4, 0, sizeof(struct quisk_cHB45Filter)); + memset(&Storage[i].HalfBand5, 0, sizeof(struct quisk_cHB45Filter)); + memset(&Storage[i].HalfBand6, 0, sizeof(struct quisk_dHB45Filter)); + memset(&Storage[i].HalfBand7, 0, sizeof(struct quisk_dHB45Filter)); + quisk_filt_dInit(&Storage[i].filtAudio48p3, quiskLpFilt48Coefs, sizeof(quiskLpFilt48Coefs)/sizeof(double)); + quisk_filt_dInit(&Storage[i].filtAudio24p3, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double)); + quisk_filt_dInit(&Storage[i].filtAudio24p4, quiskAudio24p4Coefs, sizeof(quiskAudio24p4Coefs)/sizeof(double)); + quisk_filt_dInit(&Storage[i].filtAudio12p2, quiskAudio24p4Coefs, sizeof(quiskAudio24p4Coefs)/sizeof(double)); + quisk_filt_dInit(&Storage[i].filtAudio24p6, quiskAudio24p6Coefs, sizeof(quiskAudio24p6Coefs)/sizeof(double)); + quisk_filt_dInit(&Storage[i].filtAudioFmHp, quiskAudioFmHpCoefs, sizeof(quiskAudioFmHpCoefs)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filtDecim16to8, quiskFilt16dec8Coefs, sizeof(quiskFilt16dec8Coefs)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filtDecim48to24, quiskFilt48dec24Coefs, sizeof(quiskFilt48dec24Coefs)/sizeof(double)); + quisk_filt_cInit(&Storage[i].filtDecim48to16, quiskAudio24p3Coefs, sizeof(quiskAudio24p3Coefs)/sizeof(double)); + //quisk_filt_dInit(&Storage[i].filtFMdiff, quiskDiff48Coefs, sizeof(quiskDiff48Coefs)/sizeof(double)); + //quisk_filt_differInit(&Storage[i].filtFMdiff, 9); + Storage[i].fm_1 = 10; + Storage[i].FM_www = tan(M_PI * FM_FILTER_DEMPH / 48000); // filter for FM at 48 ksps + Storage[i].FM_nnn = 1.0 / (1.0 + Storage[i].FM_www); + Storage[i].FM_a_0 = Storage[i].FM_www * Storage[i].FM_nnn; + Storage[i].FM_a_1 = Storage[i].FM_a_0; + Storage[i].FM_b_1 = Storage[i].FM_nnn * (Storage[i].FM_www - 1.0); + //QuiskPrintf ("dsamples[i] = y_1 = di * %12.6lf + x_1 * %12.6lf - y_1 * %12.6lf\n", FM_a_0, FM_a_1, FM_b_1); + } + return 0; + } + + //quisk_calc_audio_graph(pow(2, 31) - 1, cSamples, NULL, nSamples, 0); + // Filter and demodulate signal, copy capture buffer cSamples to play buffer dsamples. + // quisk_decim_srate is the sample rate after integer decimation. + MeasureSquelch[bank].squelch_active = 0; + switch(rx_mode) { + case CWL: // lower sideband CW at 6 ksps + quisk_filter_srate = quisk_decim_srate / 8; + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand5); + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand4); + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim48to24, 2); + for (i = 0; i < nSamples; i++) { + cx = cRxFilterOut(cSamples[i], bank, nFilter); + dsamples[i] = dd = creal(cx) + cimag(cx); + if(bank == 0) { + measure_audio_sum += dd * dd; + measure_audio_count += 1; + } + } + if(bank == 0) + dAutoNotch(dsamples, nSamples, rit_freq, quisk_filter_srate); + nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio12p2, 2); + nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand6); + nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7); + break; + case CWU: // upper sideband CW at 6 ksps + quisk_filter_srate = quisk_decim_srate / 8; + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand5); + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand4); + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim48to24, 2); + for (i = 0; i < nSamples; i++) { + cx = cRxFilterOut(cSamples[i], bank, nFilter); + dsamples[i] = dd = creal(cx) - cimag(cx); + if(bank == 0) { + measure_audio_sum += dd * dd; + measure_audio_count += 1; + } + } + if(bank == 0) + dAutoNotch(dsamples, nSamples, rit_freq, quisk_filter_srate); + nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio12p2, 2); + nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand6); + nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7); + break; + case LSB: // lower sideband SSB at 12 ksps + quisk_filter_srate = quisk_decim_srate / 4; + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand5); + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim48to24, 2); + for (i = 0; i < nSamples; i++) { + cx = cRxFilterOut(cSamples[i], bank, nFilter); + dsamples[i] = dd = creal(cx) + cimag(cx); + if(bank == 0) { + measure_audio_sum += dd * dd; + measure_audio_count += 1; + } + } + if(bank == 0) + dAutoNotch(dsamples, nSamples, 0, quisk_filter_srate); + if (ssb_squelch_enabled) { + ssb_squelch(dsamples, nSamples, quisk_filter_srate, MeasureSquelch + bank); + d_delay(dsamples, nSamples, bank, SQUELCH_FFT_SIZE); + } + quisk_calc_audio_graph(pow(2, 31) - 1, NULL, dsamples, nSamples, 1); + nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio24p4, 2); + nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7); + //quisk_calc_audio_graph(pow(2, 31) - 1, NULL, dsamples, nSamples, 1); + break; + case USB: // upper sideband SSB at 12 ksps + default: + quisk_filter_srate = quisk_decim_srate / 4; + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand5); + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim48to24, 2); + for (i = 0; i < nSamples; i++) { + cx = cRxFilterOut(cSamples[i], bank, nFilter); + dsamples[i] = dd = creal(cx) - cimag(cx); + if(bank == 0) { + measure_audio_sum += dd * dd; + measure_audio_count += 1; + } + } + if(bank == 0) + dAutoNotch(dsamples, nSamples, 0, quisk_filter_srate); + if (ssb_squelch_enabled) { + ssb_squelch(dsamples, nSamples, quisk_filter_srate, MeasureSquelch + bank); + d_delay(dsamples, nSamples, bank, SQUELCH_FFT_SIZE); + } + nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio24p4, 2); + nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7); + //quisk_calc_audio_graph(pow(2, 31) - 1, NULL, dsamples, nSamples, 1); + break; + case AM: // AM at 24 ksps + quisk_filter_srate = quisk_decim_srate / 2; + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim48to24, 2); + for (i = 0; i < nSamples; i++) { + cx = dRxFilterOut(cSamples[i], bank, nFilter); + di = cabs(cx); + d = di + Storage[bank].dc_remove * 0.99; // DC removal; R.G. Lyons page 553 + di = d - Storage[bank].dc_remove; + Storage[bank].dc_remove = d; + dsamples[i] = di; + if(bank == 0) { + measure_audio_sum += di * di; + measure_audio_count += 1; + } + } + nSamples = quisk_dFilter(dsamples, nSamples, &Storage[bank].filtAudio24p6); + if(bank == 0) + dAutoNotch(dsamples, nSamples, 0, quisk_filter_srate); + if (ssb_squelch_enabled) { + ssb_squelch(dsamples, nSamples, quisk_filter_srate, MeasureSquelch + bank); + d_delay(dsamples, nSamples, bank, SQUELCH_FFT_SIZE); + } + nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7); + break; + case FM: // FM at 48 ksps + case DGT_FM: + quisk_filter_srate = quisk_decim_srate; +#if 1 + for (i = 0; i < nSamples; i++) { + cx = dRxFilterOut(cSamples[i], bank, nFilter); + MeasureSquelch[bank].rf_sum += cabs(cx); + MeasureSquelch[bank].rf_count += 1; + // Phase difference in successive samples + di = carg(cx * conj(Storage[bank].fm_1)); + Storage[bank].fm_1 = cx; + dsamples[i] = di; + } +#endif +#if 0 + count += nSamples; + for (i = 0; i < nSamples; i++) { + cx = dRxFilterOut(cSamples[i], bank, nFilter); + // Integrate phase difference in successive samples and then differentiate. Phase drifts. + di = carg(cx * conj(Storage[bank].fm_1)); + Storage[bank].fm_1 = cx; + MeasureSquelch[bank].audio_sum += fabs(di); + Storage[bank].FM_phase += di; + dsamples[i] = Storage[bank].FM_phase; + } + if (count >= 48000) { + count = 0; + printf("Phase %12.4lf\n", dsamples[0]); + } + nSamples = quisk_dFilter(dsamples, nSamples, &Storage[bank].filtFMdiff); +#endif + for (i = 0; i < nSamples; i++) { + dsamples[i] *= 20e5; + di = dsamples[i]; + // FM de-emphasis + dsamples[i] = Storage[bank].FM_y_1 = di * Storage[bank].FM_a_0 + + Storage[bank].FM_x_1 * Storage[bank].FM_a_1 - Storage[bank].FM_y_1 * Storage[bank].FM_b_1; + Storage[bank].FM_x_1 = di; + } + nSamples = quisk_dDecimate(dsamples, nSamples, &Storage[bank].filtAudio48p3, 4); + nSamples = quisk_dFilter(dsamples, nSamples, &Storage[bank].filtAudioFmHp); + nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand6); + nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7); + if(bank == 0) { + dAutoNotch(dsamples, nSamples, 0, quisk_filter_srate); + for (i = 0; i < nSamples; i++) { + measure_audio_sum += dsamples[i] * dsamples[i]; + measure_audio_count += 1; + } + } + if (MeasureSquelch[bank].rf_count >= 2400) { + MeasureSquelch[bank].squelch = MeasureSquelch[bank].rf_sum / MeasureSquelch[bank].rf_count / CLIP32; + if (MeasureSquelch[bank].squelch > 1.E-10) + MeasureSquelch[bank].squelch = 20 * log10(MeasureSquelch[bank].squelch); + else + MeasureSquelch[bank].squelch = -200.0; + MeasureSquelch[bank].rf_sum = MeasureSquelch[bank].rf_count = 0; + //printf("RF %12.4lf level %12.4lf\n", MeasureSquelch[bank].squelch, squelch_level); + } + MeasureSquelch[bank].squelch_active = MeasureSquelch[bank].squelch < squelch_level; + break; + case DGT_U: // digital mode DGT-U at 48 ksps + if (filter_bandwidth[nFilter] < DGT_NARROW_FREQ) { // filter at 6 ksps + quisk_filter_srate = quisk_decim_srate / 8; + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand5); + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand4); + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim48to24, 2); + } + else { // filter at 48 ksps + quisk_filter_srate = quisk_decim_srate; + } + for (i = 0; i < nSamples; i++) { + cx = cRxFilterOut(cSamples[i], bank, nFilter); + dsamples[i] = dd = creal(cx) - cimag(cx); + if(bank == 0) { + measure_audio_sum += dd * dd; + measure_audio_count += 1; + } + } + if(bank == 0) + dAutoNotch(dsamples, nSamples, 0, quisk_filter_srate); + if (filter_bandwidth[nFilter] < DGT_NARROW_FREQ) { + nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio12p2, 2); + nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand6); + nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7); + } + break; + case DGT_L: // digital mode DGT-L + if (filter_bandwidth[nFilter] < DGT_NARROW_FREQ) { // filter at 6 ksps + quisk_filter_srate = quisk_decim_srate / 8; + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand5); + nSamples = quisk_cDecim2HB45(cSamples, nSamples, &Storage[bank].HalfBand4); + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim48to24, 2); + } + else { // filter at 48 ksps + quisk_filter_srate = quisk_decim_srate; + } + for (i = 0; i < nSamples; i++) { + cx = cRxFilterOut(cSamples[i], bank, nFilter); + dsamples[i] = dd = creal(cx) + cimag(cx); + if(bank == 0) { + measure_audio_sum += dd * dd; + measure_audio_count += 1; + } + } + if(bank == 0) + dAutoNotch(dsamples, nSamples, 0, quisk_filter_srate); + if (filter_bandwidth[nFilter] < DGT_NARROW_FREQ) { + nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio12p2, 2); + nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand6); + nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7); + } + break; + case DGT_IQ: // digital mode DGT-IQ at 48 ksps + quisk_filter_srate = quisk_decim_srate; + if (filter_bandwidth[nFilter] < 19000) { // No filtering for wide bandwidth + for (i = 0; i < nSamples; i++) + cSamples[i] = dRxFilterOut(cSamples[i], bank, nFilter); + } + if(bank == 0) { + for (i = 0; i < nSamples; i++) { + measure_audio_sum = measure_audio_sum + cSamples[i] * conj(cSamples[i]); + measure_audio_count += 1; + } + } + break; + case FDV_U: // digital voice + case FDV_L: // Extra modes added by Dave Roberts, G8KBB, June 2020. + quisk_check_freedv_mode(); + // added conditional for 2400A/B mmodes + // current coding assumes modem sample rate is 8000 16000 or 48000 + if( n_modem_sample_rate <= 16000 ) + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim48to16, 3); + if (bank == 0) + process_agc(&Agc1, cSamples, nSamples, 1); + else + process_agc(&Agc2, cSamples, nSamples, 1); + // Perhaps decimate by an additional fraction + if (quisk_decim_srate != 48000) { + dd = quisk_decim_srate / 48000.0; + nSamples = cFracDecim(cSamples, nSamples, dd); + quisk_decim_srate = 48000; + } + quisk_filter_srate = n_speech_sample_rate; + // added conditional for 2400A/B mmodes + if( n_modem_sample_rate == 8000 ) + nSamples = quisk_cDecimate(cSamples, nSamples, &Storage[bank].filtDecim16to8, 2); + // pass data decimated down to n_modem_sample_rate + // return data is as n_speech_sample_rate + if (pt_quisk_freedv_rx) + nSamples = (* pt_quisk_freedv_rx)(cSamples, dsamples, nSamples, bank); + if(bank == 0) { + for (i = 0; i < nSamples; i++) { + measure_audio_sum += dsamples[i] * dsamples[i]; + measure_audio_count += 1; + } + } + // current coding assumes speech sample rate is only 8000 or 16000 - needs tweak for others + nSamples = quisk_dInterpolate(dsamples, nSamples, &Storage[bank].filtAudio24p3, 3); + if( n_speech_sample_rate == 8000 ) + nSamples = quisk_dInterp2HB45(dsamples, nSamples, &Storage[bank].HalfBand7); + break; + } + if (bank == 0 && measure_audio_count >= quisk_filter_srate * measure_audio_time) { + measured_audio = sqrt(measure_audio_sum / measure_audio_count) / CLIP32 * 1e6; + measure_audio_sum = measure_audio_count = 0; + } + return nSamples; +} + +static void process_agc(struct AgcState * dat, complex double * csamples, int count, int is_cpx) +{ + int i; + double out_magn, buf_magn, dtmp, clip_gain; + complex double csample; +#if DEBUG + static int printit=0; + static double maxout=1; + char * clip; +#endif + + if ( ! dat->buf_size) { // initialize + if (dat->sample_rate == 0) + dat->sample_rate = quisk_sound_state.playback_rate; + dat->buf_size = dat->sample_rate * AGC_DELAY / 1000; // total delay in samples + //QuiskPrintf("play rate %d buf_size %d\n", dat->sample_rate, dat->buf_size); + dat->index_read = 0; // Index to output; and then write a new sample here + dat->index_start = 0; // Start index for measure of maximum sample + dat->is_clipping = 0; // Are we decreasing gain to handle a clipping condition? + dat->themax = 1.0; // Maximum sample in the buffer + dat->gain = 100; // Current output gain + dat->delta = 0; // Amount to change dat->gain at each sample + dat->target_gain = 100; // Move to this gain unless we clip + dat->time_release = 1.0 - exp( - 1.0 / dat->sample_rate / agc_release_time); // long time constant for AGC release + dat->c_samp = (complex double *) malloc(dat->buf_size * sizeof(complex double)); // buffer for complex samples + for (i = 0; i < dat->buf_size; i++) + dat->c_samp[i] = 0; + return; + } + for (i = 0; i < count; i++) { + csample = csamples[i]; + csamples[i] = dat->c_samp[dat->index_read] * dat->gain; // FIFO output + if (is_cpx) + out_magn = cabs(csamples[i]); + else + out_magn = fabs(creal(csamples[i])); + //if(dat->is_clipping == 1) + //QuiskPrintf(" index %5d out_magn %.5lf gain %.2lf delta %.5lf\n",dat->index_read, out_magn / CLIP32, dat->gain, dat->delta); +#if DEBUG + if (out_magn > maxout) + maxout = out_magn; +#endif + if (out_magn > CLIP32) { + csamples[i] /= out_magn; +#if DEBUG + QuiskPrintf("Clip out_magn %8.5lf is_clipping %d index_read %5d index_start %5d gain %8.5lf\n", + out_magn / CLIP32, dat->is_clipping, dat->index_read, dat->index_start, dat->gain); +#endif + } + dat->c_samp[dat->index_read] = csample; // write new sample at read index + if (is_cpx) + buf_magn = cabs(csample); + else + buf_magn = fabs(creal(csample)); + if (dat->is_clipping == 0) { + if (buf_magn * dat->gain > dat->max_out * CLIP32) { + dat->target_gain = dat->max_out * CLIP32 / buf_magn; + dat->delta = (dat->gain - dat->target_gain) / dat->buf_size; + dat->is_clipping = 1; + dat->themax = buf_magn; + // QuiskPrintf("Start index %5d buf_magn %10.8lf target %8.2lf gain %8.2lf delta %8.5lf\n", + // dat->index_read, buf_magn / CLIP32, dat->target_gain, dat->gain, dat->delta); + dat->gain -= dat->delta; + } + else if (dat->index_read == dat->index_start) { + clip_gain = dat->max_out * CLIP32 / dat->themax; // clip gain based on the maximum sample in the buffer + if (0) //rxMode == FM || rxMode == DGT_FM) // mode is FM + dat->target_gain = clip_gain; + else if (agcReleaseGain > clip_gain) + dat->target_gain = clip_gain; + else + dat->target_gain = agcReleaseGain; + dat->themax = buf_magn; + dat->gain = dat->gain * (1.0 - dat->time_release) + dat->target_gain * dat->time_release; + // QuiskPrintf("New index %5d themax %7.5lf clip_gain %5.0lf agcReleaseGain %5.0lf\n", + // dat->index_start, dat->themax / CLIP32, clip_gain, agcReleaseGain); + } + else { + if (dat->themax < buf_magn) + dat->themax = buf_magn; + dat->gain = dat->gain * (1.0 - dat->time_release) + dat->target_gain * dat->time_release; + } + } + else { // dat->is_clipping == 1; we are handling a clip condition + if (buf_magn > dat->themax) { + dat->themax = buf_magn; + dat->target_gain = dat->max_out * CLIP32 / buf_magn; + dtmp = (dat->gain - dat->target_gain) / dat->buf_size; // new value of delta + if (dtmp > dat->delta) { + dat->delta = dtmp; + // QuiskPrintf(" Strt index %5d buf_magn %10.8lf target %8.2lf gain %8.2lf delta %8.5lf\n", + // dat->index_read, buf_magn / CLIP32, dat->target_gain, dat->gain, dat->delta); + } + else { + // QuiskPrintf(" Plus index %5d buf_magn %10.8lf target %8.2lf gain %8.2lf delta %8.5lf\n", + // dat->index_read, buf_magn / CLIP32, dat->target_gain, dat->gain, dat->delta); + } + } + dat->gain -= dat->delta; + if (dat->gain <= dat->target_gain) { + dat->is_clipping = 0; + dat->gain = dat->target_gain; + // QuiskPrintf("End index %5d buf_magn %10.8lf target %8.2lf gain %8.2lf delta %8.5lf themax %10.8lf\n", + // dat->index_read, buf_magn / CLIP32, dat->target_gain, dat->gain, dat->delta, dat->themax / CLIP32); + dat->themax = buf_magn; + dat->index_start = dat->index_read; + } + } + if (++dat->index_read >= dat->buf_size) + dat->index_read = 0; +#if DEBUG + if (printit++ >= dat->sample_rate * 500 / 1000) { + printit = 0; + dtmp = 20 * log10(maxout / CLIP32); + if (dtmp >= 0) + clip = "Clip"; + else + clip = ""; + QuiskPrintf("Out agcGain %5.0lf target_gain %9.0lf gain %9.0lf output %7.2lf %s\n", + agcReleaseGain, dat->target_gain, dat->gain, dtmp, clip); + maxout = 1; + } +#endif + } + return; +} + +int quisk_process_samples(complex double * cSamples, int nSamples) +{ +// Called when samples are available. +// Samples range from about 2^16 to a max of 2^31. + int i, n, nout, squelch_real=0, squelch_imag=0; + double d, di, tune; + double double_filter_decim; + complex double phase; + int orig_nSamples; + fft_data * ptFFT; + rx_mode_type rx_mode; + + static int size_dsamples = 0; // Current dimension of dsamples, dsamples2, orig_cSamples, buf_cSamples + static int old_split_rxtx = 0; // Prior value of split_rxtx + static int old_multirx_play_channel = 0; // Prior value of multirx_play_channel + static double * dsamples = NULL; + static double * dsamples2 = NULL; + static complex double * orig_cSamples = NULL; + static complex double * buf_cSamples = NULL; + static complex double rxTuneVector = 1; + static complex double txTuneVector = 1; + static complex double aux1TuneVector = 1; + static complex double aux2TuneVector = 1; + static complex double sidetoneVector = BIG_VOLUME; + static double dOutCounter = 0; // Cumulative net output samples for sidetone etc. + static int sidetoneIsOn = 0; // The status of the sidetone + static double sidetoneEnvelope; // Shape the rise and fall times of the sidetone + static double keyupEnvelope = 1.0; // Shape the rise time on key up + static int playSilence; + static struct quisk_cHB45Filter HalfBand7 = {NULL, 0, 0}; + static struct quisk_cHB45Filter HalfBand8 = {NULL, 0, 0}; + static struct quisk_cHB45Filter HalfBand9 = {NULL, 0, 0}; + static struct AgcState Agc1 = {0.7, 0, 0}, Agc2 = {0.7, 0, 0}, Agc3 = {0.7, 0, 0}; + +#if DEBUG + static int printit; + static time_t time0; + static double levelA=0, levelB=0, levelC=0, levelD=0, levelE=0; + + if (time(NULL) != time0) { + time0 = time(NULL); + printit = 1; + } + else { + printit = 0; + } +#endif + if (nSamples <= 0) + return nSamples; + if (nSamples > size_dsamples) { + if (dsamples) + free(dsamples); + if (dsamples2) + free(dsamples2); + if (orig_cSamples) + free(orig_cSamples); + if (buf_cSamples) + free(buf_cSamples); + size_dsamples = nSamples * 2; + dsamples = (double *)malloc(size_dsamples * sizeof(double)); + dsamples2 = (double *)malloc(size_dsamples * sizeof(double)); + orig_cSamples = (complex double *)malloc(size_dsamples * sizeof(complex double)); + buf_cSamples = (complex double *)malloc(size_dsamples * sizeof(complex double)); + } + +#if SAMPLES_FROM_FILE == 1 + QuiskWavWriteC(&hWav, cSamples, nSamples); +#elif SAMPLES_FROM_FILE == 2 + QuiskWavReadC(&hWav, cSamples, nSamples); +#endif + + orig_nSamples = nSamples; + if (split_rxtx) { + memcpy(orig_cSamples, cSamples, nSamples * sizeof(complex double)); + if ( ! old_split_rxtx) // start of new split mode + Buffer2Chan(NULL, 0, NULL, 0); + } + if (multirx_play_channel != old_multirx_play_channel) // change in play channel + Buffer2Chan(NULL, 0, NULL, 0); + old_split_rxtx = split_rxtx; + old_multirx_play_channel = multirx_play_channel; + + if (quisk_is_key_down() && !quisk_isFDX) { // The key is down; replace this data block + dOutCounter += (double)nSamples * quisk_sound_state.playback_rate / + quisk_sound_state.sample_rate; + nout = (int)dOutCounter; // number of samples to output + dOutCounter -= nout; + playSilence = (int)(quisk_sound_state.playback_rate * 50e-3); // Play silence after sidetone ends, number of samples + keyupEnvelope = 0; + if (quisk_active_sidetone == 2 && QUISK_CWKEY_DOWN) { // Play sidetone instead of radio for CW + if (! sidetoneIsOn) { // turn on sidetone + sidetoneIsOn = 1; + sidetoneEnvelope = 0; + sidetoneVector = BIG_VOLUME; + } + for (i = 0 ; i < nout; i++) { + if (sidetoneEnvelope < 1.0) { + sidetoneEnvelope += 1. / (quisk_sound_state.playback_rate * 5e-3); // 5 milliseconds + if (sidetoneEnvelope > 1.0) + sidetoneEnvelope = 1.0; + } + d = creal(sidetoneVector) * quisk_sidetoneVolume * sidetoneEnvelope; + cSamples[i] = d + I * d; + sidetoneVector *= sidetonePhase; + } + } + else { // Otherwise play silence + for (i = 0 ; i < nout; i++) + cSamples[i] = 0; + } + return nout; + } + // Key is up + if(sidetoneIsOn) { // decrease sidetone until it is off + dOutCounter += (double)nSamples * quisk_sound_state.playback_rate / + quisk_sound_state.sample_rate; + nout = (int)dOutCounter; // number of samples to output + dOutCounter -= nout; + for (i = 0; i < nout; i++) { + sidetoneEnvelope -= 1. / (quisk_sound_state.playback_rate * 5e-3); // 5 milliseconds + if (sidetoneEnvelope < 0) { + sidetoneIsOn = 0; + sidetoneEnvelope = 0; + break; // sidetone is zero + } + d = creal(sidetoneVector) * quisk_sidetoneVolume * sidetoneEnvelope; + cSamples[i] = d + I * d; + sidetoneVector *= sidetonePhase; + } + for ( ; i < nout; i++) { // continue with playSilence, even if zero + cSamples[i] = 0; + playSilence--; + } + return nout; + } + if (playSilence > 0) { // Continue to play silence after the key is up + dOutCounter += (double)nSamples * quisk_sound_state.playback_rate / + quisk_sound_state.sample_rate; + nout = (int)dOutCounter; // number of samples to output + dOutCounter -= nout; + for (i = 0; i < nout; i++) + cSamples[i] = 0; + playSilence -= nout; + return nout; + } + // We are done replacing sound with a sidetone or silence. + // Filter and demodulate the samples as radio sound. + + // Add a test tone to the data + if (testtonePhase) + AddTestTone(cSamples, nSamples); + + // Invert spectrum + if (quisk_invert_spectrum) { + for (i = 0; i < nSamples; i++) { + cSamples[i] = conj(cSamples[i]); + } + } + + NoiseBlanker(cSamples, nSamples); + + // Put samples into the fft input array. + // Thanks to WB4JFI for the code to add a third FFT buffer, July 2010. + // Changed to multiple FFTs May 2014. + if (multiple_sample_rates == 0) { + ptFFT = fft_data_array + fft_data_index; + for (i = 0; i < nSamples; i++) { + ptFFT->samples[ptFFT->index] = cSamples[i]; + if (++(ptFFT->index) >= fft_size) { // check sample count + n = fft_data_index + 1; // next FFT data location + if (n >= FFT_ARRAY_SIZE) + n = 0; + if (fft_data_array[n].filled == 0) { // Is the next buffer empty? + fft_data_array[n].index = 0; + fft_data_array[n].block = 0; + fft_data_array[fft_data_index].filled = 1; // Mark the previous buffer ready. + fft_data_index = n; // Write samples into the new buffer. + ptFFT = fft_data_array + fft_data_index; + } + else { // no place to write samples + ptFFT->index = 0; + fft_error++; + } + } + } + } + + // Tune the data to frequency + if (multiple_sample_rates == 0) + tune = rx_tune_freq; + else + tune = rx_tune_freq + vfo_screen - vfo_audio; + if (tune != 0) { + phase = cexp((I * -2.0 * M_PI * tune) / quisk_sound_state.sample_rate); + for (i = 0; i < nSamples; i++) { + cSamples[i] *= rxTuneVector; + rxTuneVector *= phase; + } + } + + if (rxMode == EXT) { // External filter and demodulate + d = (double)quisk_sound_state.sample_rate / quisk_sound_state.playback_rate; // total decimation needed + nSamples = quisk_extern_demod(cSamples, nSamples, d); + goto start_agc; + } + + // Perhaps write sample data to the soundcard output without decimation + if (TEST_AUDIO == 1) { // Copy I channel capture to playback + di = 1.e4 * quisk_audioVolume; + for (i = 0; i < nSamples; i++) + cSamples[i] = creal(cSamples[i]) * di; + return nSamples; + } + else if (TEST_AUDIO == 2) { // Copy Q channel capture to playback + di = 1.e4 * quisk_audioVolume; + for (i = 0; i < nSamples; i++) + cSamples[i] = cimag(cSamples[i]) * di; + return nSamples; + } +#if DEBUG + for (i = 0; i < nSamples; i++) { + d = cabs(cSamples[i]); + if (levelA < d) + levelA = d; + } +#endif + + nSamples = quisk_process_decimate(cSamples, nSamples, 0, rxMode); + +#if DEBUG + for (i = 0; i < nSamples; i++) { + d = cabs(cSamples[i]); + if (levelB < d) + levelB = d; + } +#endif + + if (measure_freq_mode) + measure_freq(cSamples, nSamples, quisk_decim_srate); + + nSamples = quisk_process_demodulate(cSamples, dsamples, nSamples, 0, 0, rxMode); + + squelch_real = 0; // keep track of the squelch for the two play channels + squelch_imag = 0; + if (rxMode == DGT_IQ) { + ; // This mode is already stereo + } + else if (split_rxtx) { // Demodulate a second channel from the same receiver + phase = cexp((I * -2.0 * M_PI * (quisk_tx_tune_freq + rit_freq)) / quisk_sound_state.sample_rate); + // Tune the second channel to frequency + for (i = 0; i < orig_nSamples; i++) { + orig_cSamples[i] *= txTuneVector; + txTuneVector *= phase; + } + n = quisk_process_decimate(orig_cSamples, orig_nSamples, 1, rxMode); + n = quisk_process_demodulate(orig_cSamples, dsamples2, n, 1, 0, rxMode); + nSamples = Buffer2Chan(dsamples, nSamples, dsamples2, n); // buffer dsamples and dsamples2 so the count is equal + // dsamples was demodulated on bank 0, dsamples2 on bank 1 + switch(split_rxtx) { + default: + case 1: // stereo, higher frequency is real + if (quisk_tx_tune_freq < rx_tune_freq) { + squelch_real = MeasureSquelch[0].squelch_active; + squelch_imag = MeasureSquelch[1].squelch_active; + for (i = 0; i < nSamples; i++) + cSamples[i] = dsamples[i] + I * dsamples2[i]; + } + else { + squelch_real = MeasureSquelch[1].squelch_active; + squelch_imag = MeasureSquelch[0].squelch_active; + for (i = 0; i < nSamples; i++) + cSamples[i] = dsamples2[i] + I * dsamples[i]; + } + break; + case 2: // stereo, lower frequency is real + if (quisk_tx_tune_freq >= rx_tune_freq) { + squelch_real = MeasureSquelch[0].squelch_active; + squelch_imag = MeasureSquelch[1].squelch_active; + for (i = 0; i < nSamples; i++) + cSamples[i] = dsamples[i] + I * dsamples2[i]; + } + else { + squelch_real = MeasureSquelch[1].squelch_active; + squelch_imag = MeasureSquelch[0].squelch_active; + for (i = 0; i < nSamples; i++) + cSamples[i] = dsamples2[i] + I * dsamples[i]; + } + break; + case 3: // mono receive channel + squelch_real = squelch_imag = MeasureSquelch[0].squelch_active; + for (i = 0; i < nSamples; i++) + cSamples[i] = dsamples[i] + I * dsamples[i]; + break; + case 4: // mono transmit channel + squelch_real = squelch_imag = MeasureSquelch[1].squelch_active; + for (i = 0; i < nSamples; i++) + cSamples[i] = dsamples2[i] + I * dsamples2[i]; + break; + } + } + else if (multirx_play_channel >= 0 && multirx_cSamples[multirx_play_channel]) { // Demodulate a second channel from a different receiver + memcpy(buf_cSamples, multirx_cSamples[multirx_play_channel], orig_nSamples * sizeof(complex double)); + phase = cexp((I * -2.0 * M_PI * (multirx_freq[multirx_play_channel])) / quisk_sound_state.sample_rate); + // Tune the second channel to frequency + for (i = 0; i < orig_nSamples; i++) { + buf_cSamples[i] *= aux1TuneVector; + aux1TuneVector *= phase; + } + n = quisk_process_decimate(buf_cSamples, orig_nSamples, 1, multirx_mode[multirx_play_channel]); + n = quisk_process_demodulate(buf_cSamples, dsamples2, n, 1, 1, multirx_mode[multirx_play_channel]); + nSamples = Buffer2Chan(dsamples, nSamples, dsamples2, n); // buffer dsamples and dsamples2 so the count is equal + switch(multirx_play_method) { + default: + case 0: // play both + squelch_real = squelch_imag = MeasureSquelch[1].squelch_active; + for (i = 0; i < nSamples; i++) + cSamples[i] = dsamples2[i] + I * dsamples2[i]; + break; + case 1: // play left + squelch_real = MeasureSquelch[0].squelch_active; + squelch_imag = MeasureSquelch[1].squelch_active; + for (i = 0; i < nSamples; i++) + cSamples[i] = dsamples[i] + I * dsamples2[i]; + break; + case 2: // play right + squelch_real = MeasureSquelch[1].squelch_active; + squelch_imag = MeasureSquelch[0].squelch_active; + for (i = 0; i < nSamples; i++) + cSamples[i] = dsamples2[i] + I * dsamples[i]; + break; + } + } + else { // monophonic sound played on both channels + squelch_real = squelch_imag = MeasureSquelch[0].squelch_active; + for (i = 0; i < nSamples; i++) { + d = dsamples[i]; + cSamples[i] = d + I * d; + } + } + + // play sub-receiver 1 audio on a digital output device + rx_mode = multirx_mode[0]; + if (quisk_multirx_count > 0 && + (rx_mode == DGT_U || rx_mode == DGT_L || rx_mode == DGT_IQ || rx_mode == DGT_FM) && + quiskPlaybackDevices[QUISK_INDEX_SUB_RX1]->driver) { + phase = cexp((I * -2.0 * M_PI * (multirx_freq[0])) / quisk_sound_state.sample_rate); + // Tune the channel to frequency + for (i = 0; i < orig_nSamples; i++) { + multirx_cSamples[0][i] *= aux2TuneVector; + aux2TuneVector *= phase; + } + n = quisk_process_decimate(multirx_cSamples[0], orig_nSamples, 2, rx_mode); + n = quisk_process_demodulate(multirx_cSamples[0], dsamples2, n, 2, 2, rx_mode); + if (rx_mode == DGT_IQ) { // DGT-IQ + process_agc(&Agc3, multirx_cSamples[0], n, 1); + } + else { + for (i = 0; i < n; i++) + multirx_cSamples[0][i] = dsamples2[i] + I * dsamples2[i]; + process_agc(&Agc3, multirx_cSamples[0], n, 0); + } + play_sound_interface(quiskPlaybackDevices[QUISK_INDEX_SUB_RX1], n, multirx_cSamples[0], 1, 1.0); + } + + // Perhaps decimate by an additional fraction + if (quisk_decim_srate != 48000) { + double_filter_decim = quisk_decim_srate / 48000.0; + nSamples = cFracDecim(cSamples, nSamples, double_filter_decim); + quisk_decim_srate = 48000; + } + // Process the Rx path with the WDSP library + nSamples = wdspFexchange0(QUISK_WDSP_RX, (double *)cSamples, nSamples); + + // Interpolate the samples from 48000 sps to the play rate. + switch (quisk_sound_state.playback_rate / 48000) { + case 1: + break; + case 2: + nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand7); + break; + case 4: + nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand7); + nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand8); + break; + case 8: + nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand7); + nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand8); + nSamples = quisk_cInterp2HB45(cSamples, nSamples, &HalfBand9); + break; + default: + QuiskPrintf ("Failure in quisk.c in integer interpolation %d %d\n", quisk_decim_srate, quisk_sound_state.playback_rate); + break; + } + + // Find the peak signal amplitude +start_agc: + if (rxMode == EXT || rxMode == DGT_IQ) { // Ext and DGT-IQ stereo sound + process_agc(&Agc1, cSamples, nSamples, 1); + } + else if (rxMode == FDV_U || rxMode == FDV_L) { // Agc already done + ; + } + else if (split_rxtx || multirx_play_channel >= 0) { // separate AGC for left and right channels + for (i = 0; i < nSamples; i++) { + orig_cSamples[i] = cimag(cSamples[i]); + cSamples[i] = creal(cSamples[i]); + } + process_agc(&Agc1, cSamples, nSamples, 0); + process_agc(&Agc2, orig_cSamples, nSamples, 0); + for (i = 0; i < nSamples; i++) + cSamples[i] = creal(cSamples[i]) + I * creal(orig_cSamples[i]); + } + else { // monophonic sound + process_agc(&Agc1, cSamples, nSamples, 0); + } +#if DEBUG + if (printit) { + d = CLIP32; + //QuiskPrintf ("Levels: %12.8lf %12.8lf %12.8lf %12.8lf %12.8lf\n", + // levelA/d, levelB/d, levelC/d, levelD/d, levelE/d); + levelA = levelB = levelC = levelD = levelE = 0; + } +#endif + + if (kill_audio) { + squelch_real = squelch_imag = 1; + for (i = 0; i < nSamples; i++) + cSamples[i] = 0; + } + else if (squelch_real && squelch_imag) { + for (i = 0; i < nSamples; i++) + cSamples[i] = 0; + } + else if (squelch_imag) { + for (i = 0; i < nSamples; i++) + cSamples[i] = creal(cSamples[i]); + } + else if (squelch_real) { + for (i = 0; i < nSamples; i++) + cSamples[i] = I * cimag(cSamples[i]); + } + if (keyupEnvelope < 1.0) { // raise volume slowly after the key goes up + di = 1. / (quisk_sound_state.playback_rate * 5e-3); // 5 milliseconds + for (i = 0; i < nSamples; i++) { + keyupEnvelope += di; + if (keyupEnvelope > 1.0) { + keyupEnvelope = 1.0; + break; + } + cSamples[i] *= keyupEnvelope; + } + } + if (quisk_record_state == TMP_RECORD_SPEAKERS && ! (squelch_real && squelch_imag)) + quisk_tmp_record(cSamples, nSamples, 1.0); // save radio sound + return nSamples; +} + +static PyObject * get_state(PyObject * self, PyObject * args) +{ + int unused = 0; + + if (args && !PyArg_ParseTuple (args, "")) // args=NULL internal call + return NULL; + return Py_BuildValue("iiiiiNiNiiiiiiiii", + quisk_sound_state.rate_min, + quisk_sound_state.rate_max, + quisk_sound_state.sample_rate, + quisk_sound_state.chan_min, + quisk_sound_state.chan_max, + PyUnicode_DecodeUTF8(quisk_sound_state.msg1, strlen(quisk_sound_state.msg1), "replace"), + unused, + PyUnicode_DecodeUTF8(quisk_sound_state.err_msg, strlen(quisk_sound_state.err_msg), "replace"), + quisk_sound_state.read_error, + quisk_sound_state.write_error, + quisk_sound_state.underrun_error, + quisk_sound_state.latencyCapt, + quisk_sound_state.latencyPlay, + quisk_sound_state.interrupts, + fft_error, + mic_max_display, + quisk_sound_state.data_poll_usec + ); +} + +static PyObject * get_squelch(PyObject * self, PyObject * args) +{ + int freq; + + if (!PyArg_ParseTuple (args, "i", &freq)) + return NULL; + return PyInt_FromLong(IsSquelch(freq)); +} + +static PyObject * get_overrange(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + return PyInt_FromLong(quisk_get_overrange()); +} + +static PyObject * get_filter_rate(PyObject * self, PyObject * args) +{ // Return the filter sample rate as used by quisk_process_samples. + // Changes to quisk_process_decimate or quisk_process_demodulate will require changes here. + int rate, decim_srate, filter_srate, mode, bandwidth; + // mode is -1 to use the rxMode + if (!PyArg_ParseTuple (args, "ii", &mode, &bandwidth)) + return NULL; + rate = quisk_sound_state.sample_rate; + switch((rate + 100) / 1000) { + case 41: + decim_srate = 48000; + case 53: // SDR-IQ + decim_srate = rate; + break; + case 111: // SDR-IQ + decim_srate = rate / 2; + break; + case 133: // SDR-IQ + decim_srate = rate / 2; + break; + case 185: // SDR-IQ + decim_srate = rate / 3; + break; + case 370: + decim_srate = rate / 6; + break; + case 740: + decim_srate = rate / 12; + break; + case 1333: + decim_srate = rate / 24; + break; + default: + decim_srate = PlanDecimation(NULL, NULL, NULL); + break; + } + if (mode < 0) { + mode = rxMode; + bandwidth = filter_bandwidth[0]; + } + switch(mode) { + case CWL: // lower sideband CW at 6 ksps + case CWU: // upper sideband CW at 6 ksps + filter_srate = decim_srate / 8; + break; + case LSB: // lower sideband SSB at 12 ksps + case USB: // upper sideband SSB at 12 ksps + default: + filter_srate = decim_srate / 4; + break; + case AM: // AM at 24 ksps + filter_srate = decim_srate / 2; + break; + case FM: // FM at 48 ksps + case DGT_FM: // digital FM at 48 ksps + filter_srate = decim_srate; + break; + case DGT_U: // digital modes DGT-* + case DGT_L: + if (bandwidth < DGT_NARROW_FREQ) + filter_srate = decim_srate / 8; + else + filter_srate = decim_srate; + break; + case DGT_IQ: // digital mode at 48 ksps + filter_srate = decim_srate; + break; + case FDV_U: // digital voice + case FDV_L: + filter_srate = n_speech_sample_rate; + break; + } + //QuiskPrintf("Filter rate %d\n", filter_srate); + return PyInt_FromLong(filter_srate); +} + +static PyObject * get_smeter(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + return PyFloat_FromDouble(Smeter); +} + +static PyObject * get_hermes_adc(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + return PyFloat_FromDouble(hermes_adc_level); +} + +static void init_bandscope(void) +{ + int i, j; + + if (bandscope_size > 0) { + bandscopePixels = (double *)malloc(graph_width * sizeof(double)); + bandscopeSamples = (double *)malloc(bandscope_size * sizeof(double)); + bandscopeWindow = (double *)malloc(bandscope_size * sizeof(double)); + bandscopeAverage = (double *)malloc((bandscope_size / 2 + 1 + 1) * sizeof(double)); + bandscopeFFT = (complex double *)malloc((bandscope_size / 2 + 1) * sizeof(complex double)); + bandscopePlan = fftw_plan_dft_r2c_1d(bandscope_size, bandscopeSamples, bandscopeFFT, FFTW_MEASURE); + // Create the fft window + for (i = 0, j = -bandscope_size / 2; i < bandscope_size; i++, j++) + bandscopeWindow[i] = 0.5 + 0.5 * cos(2. * M_PI * j / bandscope_size); // Hanning + // zero the average array + for (i = 0; i < bandscope_size / 2 + 1; i++) + bandscopeAverage[i] = 0; + } +} + +static PyObject * add_rx_samples(PyObject * self, PyObject * args) +{ + int i; + int ii, qq; // ii, qq must be four bytes + unsigned char * pt_ii; + unsigned char * pt_qq; + Py_buffer view; + PyObject * samples; + + if (!PyArg_ParseTuple (args, "O", &samples)) + return NULL; + if ( ! PyObject_CheckBuffer(samples)) { + QuiskPrintf("add_rx_samples: Invalid object sent as samples\n"); + Py_INCREF (Py_None); + return Py_None; + } + if (PyObject_GetBuffer(samples, &view, PyBUF_SIMPLE) != 0) { + QuiskPrintf("add_rx_samples: Can not view sample buffer\n"); + Py_INCREF (Py_None); + return Py_None; + } + if (view.len % (py_sample_rx_bytes * 2) != 0) { + QuiskPrintf ("add_rx_samples: Odd number of bytes in sample buffer\n"); + } + else if (PySampleCount + view.len / py_sample_rx_bytes / 2 > SAMP_BUFFER_SIZE * 8 / 10) { + QuiskPrintf ("add_rx_samples: buffer is too full\n"); + } + else if (py_sample_rx_endian == 0) { // byte order of samples is little-endian + void * buf; + void * buf_end; + buf = view.buf; + buf_end = buf + view.len; + pt_ii = (unsigned char *)&ii + 4 - py_sample_rx_bytes; + pt_qq = (unsigned char *)&qq + 4 - py_sample_rx_bytes; + while (buf < buf_end) { + ii = qq = 0; + memcpy(pt_ii, buf, py_sample_rx_bytes); + buf += py_sample_rx_bytes; + memcpy(pt_qq, buf, py_sample_rx_bytes); + buf += py_sample_rx_bytes; + PySampleBuf[PySampleCount++] = ii + qq * I; + } + } + else { // byte order of samples is big-endian + unsigned char * buf; + unsigned char * buf_end; + buf = view.buf; + buf_end = buf + view.len; + while (buf < buf_end) { + ii = qq = 0; + pt_ii = (unsigned char *)&ii + 3; + pt_qq = (unsigned char *)&qq + 3; + for (i = 0; i < py_sample_rx_bytes; i++) + *pt_ii-- = *buf++; + for (i = 0; i < py_sample_rx_bytes; i++) + *pt_qq-- = *buf++; + PySampleBuf[PySampleCount++] = ii + qq * I; + } + } + PyBuffer_Release(&view); + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * add_bscope_samples(PyObject * self, PyObject * args) +{ + int i, count; + int ii; // ii must be four bytes + unsigned char * pt_ii; + Py_buffer view; + PyObject * samples; + + if (!PyArg_ParseTuple (args, "O", &samples)) + return NULL; + if (bandscope_size <= 0) { + QuiskPrintf("add_bscope_samples: The bandscope was not initialized with InitBscope()\n"); + Py_INCREF (Py_None); + return Py_None; + } + if ( ! PyObject_CheckBuffer(samples)) { + QuiskPrintf("add_bscope_samples: Invalid object sent as samples\n"); + Py_INCREF (Py_None); + return Py_None; + } + if (PyObject_GetBuffer(samples, &view, PyBUF_SIMPLE) != 0) { + QuiskPrintf("add_bscope_samples: Can not view sample buffer\n"); + Py_INCREF (Py_None); + return Py_None; + } + count = 0; + if (view.len != bandscope_size * py_bscope_bytes) { + QuiskPrintf ("add_bscope_samples: Wrong number of bytes in sample buffer\n"); + } + else if (py_bscope_endian == 0) { // byte order of samples is little-endian + void * buf; + void * buf_end; + buf = view.buf; + buf_end = buf + view.len; + pt_ii = (unsigned char *)&ii + 4 - py_bscope_bytes; + while (buf < buf_end) { + ii = 0; + memcpy(pt_ii, buf, py_bscope_bytes); + buf += py_bscope_bytes; + bandscopeSamples[count++] = (double)ii / CLIP32; + } + } + else { // byte order of samples is big-endian + unsigned char * buf; + unsigned char * buf_end; + buf = view.buf; + buf_end = buf + view.len; + while (buf < buf_end) { + ii = 0; + pt_ii = (unsigned char *)&ii + 3; + for (i = 0; i < py_bscope_bytes; i++) + *pt_ii-- = *buf++; + bandscopeSamples[count++] = (double)ii / CLIP32; + } + } + PyBuffer_Release(&view); + bandscopeState = 99; + Py_INCREF (Py_None); + return Py_None; +} + +static void py_sample_start(void) +{ +} + +static void py_sample_stop(void) +{ + if (bandscopePlan) { + fftw_destroy_plan(bandscopePlan); + bandscopePlan = NULL; + } +} + +static int py_sample_read(complex double * cSamples) +{ + int n; + + memcpy(cSamples, PySampleBuf, PySampleCount * sizeof(complex double)); + n = PySampleCount; + PySampleCount = 0; + return n; +} + +static PyObject * get_params(PyObject * self, PyObject * args) +{ + const char * name; + + if (!PyArg_ParseTuple (args, "s", &name)) + return NULL; + if (strcmp(name, "QUISK_HAVE_PULSEAUDIO") == 0) { +#ifdef QUISK_HAVE_PULSEAUDIO + return PyInt_FromLong(1); +#else + return PyInt_FromLong(0); +#endif + } + if (strcmp(name, "rx_udp_started") == 0) + return PyInt_FromLong(quisk_rx_udp_started); + if (strcmp(name, "serial_ptt") == 0) + return PyInt_FromLong(quisk_serial_ptt); + if (strcmp(name, "hl2_txbuf_errors") == 0) + return PyInt_FromLong(hl2_txbuf_errors); + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * write_fftw_wisdom(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + fftw_export_wisdom_to_filename(fftw_wisdom_name); + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * read_fftw_wisdom(PyObject * self, PyObject * args) +{ + char * wisdom; + PyObject * pyBytes; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + wisdom = fftw_export_wisdom_to_string(); + pyBytes = PyByteArray_FromStringAndSize(wisdom, strlen(wisdom)); + free(wisdom); + return pyBytes; +} + +static PyObject * set_params(PyObject * self, PyObject * args, PyObject * keywds) +{ /* Call with keyword arguments ONLY; change local parameters */ + static char * kwlist[] = {"quisk_is_vna", "rx_bytes", "rx_endian", "read_error", "clip", + "bscope_bytes", "bscope_endian", "bscope_size", "bandscopeScale", "hermes_pause", NULL} ; + int i, nbytes, read_error, clip, bscope_size, hermes_pause; + + nbytes = read_error = clip = bscope_size = hermes_pause = -1; + if (!PyArg_ParseTupleAndKeywords (args, keywds, "|iiiiiiiidi", kwlist, + &quisk_is_vna, &nbytes, &py_sample_rx_endian, &read_error, &clip, + &py_bscope_bytes, &py_bscope_endian, &bscope_size, &bandscopeScale, &hermes_pause)) + return NULL; + if (nbytes != -1) { + py_sample_rx_bytes = nbytes; + quisk_sample_source4(py_sample_start, py_sample_stop, py_sample_read, NULL); + } + if (read_error != -1) + quisk_sound_state.read_error++; + if (clip != -1) + quisk_sound_state.overrange++; + if (bscope_size > 0) { + if (bandscope_size == 0) { + bandscope_size = bscope_size; + init_bandscope(); + } + else if (bscope_size != bandscope_size) { + QuiskPrintf ("Illegal attempt to change bscope_size\n"); + } + } + if (hermes_pause != -1) { + i = quisk_multirx_state; + if (hermes_pause) { // pause the hermes samples + if (quisk_multirx_state < 20) + quisk_multirx_state = 20; + } + else { // resume the hermes samples + if (quisk_multirx_state >= 20) + quisk_multirx_state = 0; + } + return PyInt_FromLong(i); + } + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * get_hermes_TFRC(PyObject * self, PyObject * args) +{ // return average temperature, forward and reverse power and current + PyObject * ret; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + if (hermes_count_temperature > 0) { + hermes_temperature /= hermes_count_temperature; + hermes_fwd_power /= hermes_count_temperature; + } + else { + hermes_temperature = 0.0; + hermes_fwd_power = 0.0; + } + if (hermes_count_current > 0) { + hermes_rev_power /= hermes_count_current; + hermes_pa_current /= hermes_count_current; + } + else { + hermes_rev_power = 0.0; + hermes_pa_current = 0.0; + } + ret = Py_BuildValue("dddddd", hermes_temperature, hermes_fwd_power, hermes_rev_power, hermes_pa_current, hermes_fwd_peak, hermes_rev_peak); + hermes_temperature = 0; + hermes_fwd_power = 0; + hermes_rev_power = 0; + hermes_fwd_peak = 0; + hermes_rev_peak = 0; + hermes_pa_current = 0; + hermes_count_temperature = 0; + hermes_count_current = 0; + return ret; +} + +static PyObject * measure_frequency(PyObject * self, PyObject * args) +{ + int mode; + + if (!PyArg_ParseTuple (args, "i", &mode)) + return NULL; + if (mode >= 0) // mode >= 0 set the mode; mode < 0, just return the frequency + measure_freq_mode = mode; + return PyFloat_FromDouble(measured_frequency); +} + +static PyObject * measure_audio(PyObject * self, PyObject * args) +{ + int time; + + if (!PyArg_ParseTuple (args, "i", &time)) + return NULL; + if (time > 0) // set the average time + measure_audio_time = time; + return PyFloat_FromDouble(measured_audio); +} + +static PyObject * add_tone(PyObject * self, PyObject * args) +{ /* Add a test tone to the captured audio data */ + int freq; + + if (!PyArg_ParseTuple (args, "i", &freq)) + return NULL; + if (freq && quisk_sound_state.sample_rate) + testtonePhase = cexp((I * 2.0 * M_PI * freq) / quisk_sound_state.sample_rate); + else + testtonePhase = 0; + Py_INCREF (Py_None); + return Py_None; +} + +static void close_udp(void) +{ + short msg = 0x7373; // shutdown + + quisk_using_udp = 0; + if (rx_udp_socket != INVALID_SOCKET) { + shutdown(rx_udp_socket, QUISK_SHUT_RD); + send(rx_udp_socket, (char *)&msg, 2, 0); + send(rx_udp_socket, (char *)&msg, 2, 0); + QuiskSleepMicrosec(3000000); + close(rx_udp_socket); + rx_udp_socket = INVALID_SOCKET; + } + quisk_rx_udp_started = 0; +#ifdef MS_WINDOWS + if (cleanupWSA) { + cleanupWSA = 0; + WSACleanup(); + } +#endif +} + +static void close_udp10(void) // Metis-Hermes protocol +{ + int i; + unsigned char buf[64]; + + quisk_using_udp = 0; + if (rx_udp_socket != INVALID_SOCKET) { + shutdown(rx_udp_socket, QUISK_SHUT_RD); + buf[0] = 0xEF; + buf[1] = 0xFE; + buf[2] = 0x04; + buf[3] = 0x00; + for (i = 4; i < 64; i++) + buf[i] = 0; + send(rx_udp_socket, (char *)buf, 64, 0); + QuiskSleepMicrosec(5000); + send(rx_udp_socket, (char *)buf, 64, 0); + QuiskSleepMicrosec(2000000); + close(rx_udp_socket); + rx_udp_socket = INVALID_SOCKET; + } + quisk_rx_udp_started = 0; + quisk_multirx_state = 0; + if (bandscopePlan) { + fftw_destroy_plan(bandscopePlan); + bandscopePlan = NULL; + } +#ifdef MS_WINDOWS + if (cleanupWSA) { + cleanupWSA = 0; + WSACleanup(); + } +#endif +} + +static PyObject * close_rx_udp(PyObject * self, PyObject * args) +{ // Not necessary to call from Python because close_udp() is called from sound.c + if (!PyArg_ParseTuple (args, "")) + return NULL; + //close_udp(); + Py_INCREF (Py_None); + return Py_None; +} + +static int quisk_read_rx_udp(complex double * samp) // Read samples from UDP +{ // Size of complex sample array is SAMP_BUFFER_SIZE + ssize_t bytes; + unsigned char buf[1500]; // Maximum Ethernet is 1500 bytes. + static unsigned char seq0; // must be 8 bits + int i, n, nSamples, xr, xi, index, want_samples; + unsigned char * ptxr, * ptxi; + struct timeval tm_wait; + fd_set fds; + + // Data from the receiver is little-endian + if ( ! rx_udp_gain_correct) { + int dec; + dec = (int)(rx_udp_clock / quisk_sound_state.sample_rate + 0.5); + if ((dec / 5) * 5 == dec) // Decimation by a factor of 5 + rx_udp_gain_correct = 1.31072; + else // Decimation by factors of two + rx_udp_gain_correct = 1.0; + } + if ( ! quisk_rx_udp_started) { // we never received any data + // send our return address until we receive UDP blocks + tm_wait.tv_sec = 0; + tm_wait.tv_usec = 5000; + FD_ZERO (&fds); + FD_SET (rx_udp_socket, &fds); + if (select (rx_udp_socket + 1, &fds, NULL, NULL, &tm_wait) == 1) { // see if data is available + bytes = recv(rx_udp_socket, (char *)buf, 1500, 0); // throw away the first block + seq0 = buf[0] + 1; // Next expected sequence number + quisk_rx_udp_started = 1; +#if DEBUG_IO + QuiskPrintf("Udp data started\n"); +#endif + } + else { // send our return address to the sample source + buf[0] = buf[1] = 0x72; // UDP command "register return address" + send(rx_udp_socket, (char *)buf, 2, 0); + return 0; + } + } + nSamples = 0; + want_samples = (int)(quisk_sound_state.data_poll_usec * 1e-6 * quisk_sound_state.sample_rate + 0.5); + while (nSamples < want_samples) { // read several UDP blocks + tm_wait.tv_sec = 0; + tm_wait.tv_usec = 100000; // Linux seems to have problems with very small time intervals + FD_ZERO (&fds); + FD_SET (rx_udp_socket, &fds); + i = select (rx_udp_socket + 1, &fds, NULL, NULL, &tm_wait); + if (i == 1) + ; + else if (i == 0) { +#if DEBUG_IO + QuiskPrintf("Udp socket timeout\n"); +#endif + return 0; + } + else { +#if DEBUG_IO + QuiskPrintf("Udp select error %d\n", i); +#endif + return 0; + } + bytes = recv(rx_udp_socket, (char *)buf, 1500, 0); // blocking read + if (bytes != RX_UDP_SIZE) { // Known size of sample block + quisk_sound_state.read_error++; +#if DEBUG_IO + QuiskPrintf("read_rx_udp: Bad block size\n"); +#endif + continue; + } + // buf[0] is the sequence number + // buf[1] is the status: + // bit 0: key up/down state + // bit 1: set for ADC overrange (clip) + if (buf[0] != seq0) { +#if DEBUG_IO + QuiskPrintf("read_rx_udp: Bad sequence want %3d got %3d\n", + (unsigned int)seq0, (unsigned int)buf[0]); +#endif + quisk_sound_state.read_error++; + } + seq0 = buf[0] + 1; // Next expected sequence number + n = buf[1] & 0x01; // bit zero is key state and the PTT state + quisk_hardware_cwkey = n; + hardware_ptt = n; + if (quisk_hardware_cwkey != old_hardware_cwkey) { + old_hardware_cwkey = quisk_hardware_cwkey; + quisk_set_play_state(); + } + if (buf[1] & 0x02) // bit one is ADC overrange + quisk_sound_state.overrange++; + index = 2; + ptxr = (unsigned char *)&xr; + ptxi = (unsigned char *)ξ + // convert 24-bit samples to 32-bit samples; int must be 32 bits. + if (is_little_endian) { + while (index < bytes) { // This works for 3, 2, 1 byte samples + xr = xi = 0; + memcpy (ptxr + (4 - sample_bytes), buf + index, sample_bytes); + index += sample_bytes; + memcpy (ptxi + (4 - sample_bytes), buf + index, sample_bytes); + index += sample_bytes; + samp[nSamples++] = (xr + xi * I) * rx_udp_gain_correct; + xr = xi = 0; + memcpy (ptxr + (4 - sample_bytes), buf + index, sample_bytes); + index += sample_bytes; + memcpy (ptxi + (4 - sample_bytes), buf + index, sample_bytes); + index += sample_bytes; + samp[nSamples++] = (xr + xi * I) * rx_udp_gain_correct; + } + } + else { // big-endian + while (index < bytes) { // This works for 3-byte samples only + *(ptxr ) = buf[index + 2]; + *(ptxr + 1) = buf[index + 1]; + *(ptxr + 2) = buf[index ]; + *(ptxr + 3) = 0; + index += 3; + *(ptxi ) = buf[index + 2]; + *(ptxi + 1) = buf[index + 1]; + *(ptxi + 2) = buf[index ]; + *(ptxi + 3) = 0; + index += 3; + samp[nSamples++] = (xr + xi * I) * rx_udp_gain_correct;; + *(ptxr ) = buf[index + 2]; + *(ptxr + 1) = buf[index + 1]; + *(ptxr + 2) = buf[index ]; + *(ptxr + 3) = 0; + index += 3; + *(ptxi ) = buf[index + 2]; + *(ptxi + 1) = buf[index + 1]; + *(ptxi + 2) = buf[index ]; + *(ptxi + 3) = 0; + index += 3; + samp[nSamples++] = (xr + xi * I) * rx_udp_gain_correct;; + + } + } + } + return nSamples; +} + +static int quisk_hermes_is_ready(int rx_udp_socket) +{ // Start Hermes; return 1 when we are ready to receive data + unsigned char buf[1500]; + int i, dummy; + struct timeval tm_wait; + fd_set fds; + + if (rx_udp_socket == INVALID_SOCKET) + return 0; + switch (quisk_multirx_state) { + case 0: // Start or restart + case 20: // Temporary shutdown + quisk_rx_udp_started = 0; + buf[0] = 0xEF; + buf[1] = 0xFE; + buf[2] = 0x04; + buf[3] = 0x00; + for (i = 4; i < 64; i++) + buf[i] = 0; + send(rx_udp_socket, (char *)buf, 64, 0); // send Stop + quisk_multirx_state++; + QuiskSleepMicrosec(2000); + return 0; + case 1: + case 21: + buf[0] = 0xEF; + buf[1] = 0xFE; + buf[2] = 0x04; + buf[3] = 0x00; + for (i = 4; i < 64; i++) + buf[i] = 0; + send(rx_udp_socket, (char *)buf, 64, 0); // send Stop + quisk_multirx_state++; + QuiskSleepMicrosec(9000); + return 0; + case 2: + case 22: + while (1) { + tm_wait.tv_sec = 0; // throw away all pending records + tm_wait.tv_usec = 0; + FD_ZERO (&fds); + FD_SET (rx_udp_socket, &fds); + if (select (rx_udp_socket + 1, &fds, NULL, NULL, &tm_wait) != 1) + break; + recv(rx_udp_socket, (char *)buf, 1500, 0); + } + // change to state 3 for startup + // change to state 23 for temporary shutdown + quisk_multirx_state++; + return 0; + case 3: + quisk_multirx_count = quisk_pc_to_hermes[3] >> 3 & 0x7; // number of receivers + for (i = 0; i < quisk_multirx_count; i++) + if ( ! multirx_fft_data[i].samples) // Check that buffer exists + multirx_fft_data[i].samples = (fftw_complex *)malloc(multirx_fft_width * sizeof(fftw_complex)); + quisk_hermes_tx_send(0, NULL); + quisk_multirx_state++; + return 0; + case 4: + case 5: + case 6: + case 7: + dummy = 999999; // enable transmit + quisk_hermes_tx_send(rx_udp_socket, &dummy); // send packets with number of receivers + quisk_multirx_state++; + QuiskSleepMicrosec(2000); + return 0; + case 8: + if (quisk_rx_udp_started) { + quisk_multirx_state++; + } + else { + // send our return address until we receive UDP blocks + buf[0] = 0xEF; + buf[1] = 0xFE; + buf[2] = 0x04; + if (enable_bandscope) + buf[3] = 0x03; + else + buf[3] = 0x01; + for (i = 4; i < 64; i++) + buf[i] = 0; + send(rx_udp_socket, (char *)buf, 64, 0); + QuiskSleepMicrosec(2000); + } + return 1; + case 9: // running state; we have received UDP blocks + default: + return 1; + case 23: // we are in a temporary shutdown + return 0; + } +} + +static int read_rx_udp10(complex double * samp) // Read samples from UDP using the Hermes protocol. +{ // Size of complex sample array is SAMP_BUFFER_SIZE. Called from the sound thread. + ssize_t bytes; + unsigned char buf[1500]; + unsigned int seq; + unsigned int power; + static unsigned int seq0; + static int tx_records; + static int max_multirx_count=0; + int i, j, nSamples, xr, xi, index, start, want_samples, dindex, num_records; + complex double c; + struct timeval tm_wait; + fd_set fds; + + if ( ! quisk_hermes_is_ready(rx_udp_socket)) { + seq0 = 0; + tx_records = 0; + quisk_rx_udp_started = 0; + multirx_fft_next_index = 0; + multirx_fft_next_state = 0; + for (i = 0; i < QUISK_MAX_SUB_RECEIVERS; i++) + multirx_fft_data[i].index = 0; + return 0; + } + nSamples = 0; + want_samples = (int)(quisk_sound_state.data_poll_usec * 1e-6 * quisk_sound_state.sample_rate + 0.5); + num_records = 504 / ((quisk_multirx_count + 1) * 6 + 2); // number of samples in each of two blocks for each receiver + if (quisk_multirx_count) { + if (multirx_sample_size < want_samples + 2000) { + multirx_sample_size = want_samples * 2 + 2000; + for (i = 0; i < max_multirx_count; i++) { + free(multirx_cSamples[i]); + multirx_cSamples[i] = (complex double *)malloc(multirx_sample_size * sizeof(complex double)); + } + } + if (quisk_multirx_count > max_multirx_count) { + for (i = max_multirx_count; i < quisk_multirx_count; i++) + multirx_cSamples[i] = (complex double *)malloc(multirx_sample_size * sizeof(complex double)); + max_multirx_count = quisk_multirx_count; + } + } + while (nSamples < want_samples) { // read several UDP blocks + tm_wait.tv_sec = 0; + tm_wait.tv_usec = 100000; // Linux seems to have problems with very small time intervals + FD_ZERO (&fds); + FD_SET (rx_udp_socket, &fds); + i = select (rx_udp_socket + 1, &fds, NULL, NULL, &tm_wait); // blocking wait + if (i == 1) + ; + else if (i == 0) { +#if DEBUG_IO + QuiskPrintf("Udp socket timeout\n"); +#endif + return 0; + } + else { +#if DEBUG_IO + QuiskPrintf("Udp select error %d\n", i); +#endif + return 0; + } + bytes = recv(rx_udp_socket, (char *)buf, 1500, 0); // blocking read + if (bytes != 1032 || buf[0] != 0xEF || buf[1] != 0xFE || buf[2] != 0x01) { // Known size of sample block + quisk_sound_state.read_error++; +#if DEBUG_IO + QuiskPrintf("read_rx_udp10: Bad block size %d or header\n", (int)bytes); +#endif + return 0; + } + //// Bandscope data - reversed byte order ????? + if (buf[3] == 0x04 && bandscopeSamples) { // ADC samples for bandscope + seq = buf[7]; // sequence number + seq = seq & (bandscopeBlockCount - 1); // 0, 1, 2, ... + switch (bandscopeState) { + case 0: // Start - wait for the start of a block and record block one + if (seq == 0) { + for (i = 0, j = 8; i < 512; i++, j+= 2) + bandscopeSamples[i] = ((double)(short)(buf[j + 1] << 8 | buf[j])) / bandscopeScale; + bandscopeState = 1; + } + break; + default: + case 1: // Record blocks + if (seq == bandscopeState) { + for (i = 0, j = 8; i < 512; i++, j+= 2) + bandscopeSamples[i + 512 * seq] = ((double)(short)(buf[j + 1] << 8 | buf[j])) / bandscopeScale; + if (++bandscopeState >= bandscopeBlockCount) + bandscopeState = 99; + } + else { + bandscopeState = 0; // Error + } + break; + case 99: // wait until the complete block is used + break; + } + continue; + } + //// ADC Rx samples + if (buf[3] != 0x06) // End point 6: I/Q and mic samples + return 0; + seq = buf[4] << 24 | buf[5] << 16 | buf[6] << 8 | buf[7]; // sequence number + quisk_rx_udp_started = 1; + tx_records += num_records * 2; // total samples for each receiver + quisk_hermes_tx_send(rx_udp_socket, &tx_records); // send Tx samples, decrement tx_records + if (seq != seq0) { +#if DEBUG_IO + QuiskPrintf("read_rx_udp10: Bad sequence want %d got %d\n", seq0, seq); +#endif + quisk_sound_state.read_error++; + } + seq0 = seq + 1; // Next expected sequence number + for (start = 11; start < 1000; start += 512) { + // check the sync bytes + if (buf[start - 3] != 0x7F || buf[start - 2] != 0x7F || buf[start - 1] != 0x7F) { +#if DEBUG_IO + QuiskPrintf("read_rx_udp10: Bad sync byte\n"); +#endif + quisk_sound_state.read_error++; + } + // read five bytes of control information. start is the index of C0. + // Changes for HermesLite v2 thanks to Steve, KF7O + dindex = buf[start] >> 1; + if (dindex & 0x40) { // the ACK bit C0[7] is set + if (quisk_hermeslite_writepointer > 0) { + // Save response + quisk_hermeslite_response[0] = buf[start]; + quisk_hermeslite_response[1] = buf[start+1]; + quisk_hermeslite_response[2] = buf[start+2]; + quisk_hermeslite_response[3] = buf[start+3]; + quisk_hermeslite_response[4] = buf[start+4]; + // Look for match + if (dindex == 0x7f) { + QuiskPrintf("ERROR: Hermes-Lite did not process ACK command. Send again.\n"); + quisk_hermeslite_writepointer = 1; + } else if (dindex != (quisk_hermeslite_writequeue[0])) { + QuiskPrintf("ERROR: Nonmatching Hermes-Lite ACK response 0x%X seen\n",dindex); + } else { + //QuiskPrintf("Response received queue 0x%X 0x%X 0x%X 0x%X 0x%X\n", quisk_hermeslite_writequeue[0], + //quisk_hermeslite_writequeue[1], quisk_hermeslite_writequeue[2], quisk_hermeslite_writequeue[3], quisk_hermeslite_writequeue[4]); + quisk_hermeslite_writepointer = 0; + } + } else { + QuiskPrintf("ERROR: ACK response for 0x%X but no request outstanding\n",dindex); + } + } else { + dindex = dindex >> 2; + } + // this does not save data for Hermes-Lite ACK + if (dindex >= 0 && dindex <= 4) { // Save the data returned by the hardware + quisk_hermes_to_pc[dindex * 4 ] = buf[start + 1]; // C1 to C4 + quisk_hermes_to_pc[dindex * 4 + 1] = buf[start + 2]; + quisk_hermes_to_pc[dindex * 4 + 2] = buf[start + 3]; + quisk_hermes_to_pc[dindex * 4 + 3] = buf[start + 4]; + } + if (dindex == 0) { // C0 is 0b00000xxx +//QuiskPrintTime("Poll key change", 0); + //code_version = quisk_hermes_to_pc[3]; + if ((quisk_hermes_to_pc[0] & 0x01) != 0) // C1 + quisk_sound_state.overrange++; + hardware_ptt = buf[start] & 0x01; // C0 bit zero is PTT + quisk_hardware_cwkey = (buf[start] & 0x04) >> 2; // C0 bit two is CW key state + switch (hl2_txbuf_state) { + case 0: // hermes_mox_bit is zero. + default: + if (hermes_mox_bit) { + hl2_txbuf_state = 1; + //QuiskPrintf ("Change hermes_mox_bit %d\n", hermes_mox_bit); + } + break; + case 1: // hermes_mox_bit changed to 1 + if (hermes_mox_bit == 0) { + //QuiskPrintf ("Change hermes_mox_bit %d\n", hermes_mox_bit); + hl2_txbuf_state = 0; + } + else if (quisk_hermes_to_pc[2] & 0x7F) { // check for samples in the HL2 Tx buffer + hl2_txbuf_state = 2; + } + break; + case 2: // initial samples are in the buffer + if (hermes_mox_bit == 0) { + //QuiskPrintf ("Change hermes_mox_bit %d\n", hermes_mox_bit); + hl2_txbuf_state = 0; + } + else if (quisk_hermes_to_pc[2] == 0x80 || quisk_hermes_to_pc[2] == 0xFF) { // check for errors + hl2_txbuf_errors++; + //QuiskPrintf("FAULT quisk_hermes_to_pc[2] 0x%X\n", quisk_hermes_to_pc[2]); + hl2_txbuf_state = 3; + } + break; + case 3: // the error bit was set; wait for it to clear + if (hermes_mox_bit == 0) { + //QuiskPrintf ("Change hermes_mox_bit %d\n", hermes_mox_bit); + hl2_txbuf_state = 0; + } + else if ((quisk_hermes_to_pc[2] & 0x80) == 0) { + hl2_txbuf_state = 2; + } + break; + } + if (quisk_hardware_cwkey != old_hardware_cwkey) { +//QuiskPrintTime("Udp10 change key", 0); + old_hardware_cwkey = quisk_hardware_cwkey; + quisk_set_play_state(); + } + } + else if(dindex == 1) { // temperature and forward power + hermes_temperature += quisk_hermes_to_pc[4] << 8 | quisk_hermes_to_pc[5]; + power = quisk_hermes_to_pc[6] << 8 | quisk_hermes_to_pc[7]; + hermes_fwd_power += power; + hermes_fwd_peak = fmax(hermes_fwd_peak, (double)power); + hermes_count_temperature++; + } + else if (dindex == 2) { // reverse power and current + power = quisk_hermes_to_pc[8] << 8 | quisk_hermes_to_pc[9]; + hermes_rev_power += power; + hermes_rev_peak = fmax(hermes_rev_peak, (double)power); + hermes_pa_current += quisk_hermes_to_pc[10] << 8 | quisk_hermes_to_pc[11]; + hermes_count_current++; + } + // convert 24-bit samples to 32-bit samples; int must be 32 bits. + index = start + 5; + for (i = 0; i < num_records; i++) { // read records + xi = buf[index ] << 24 | buf[index + 1] << 16 | buf[index + 2] << 8; + xr = buf[index + 3] << 24 | buf[index + 4] << 16 | buf[index + 5] << 8; + samp[nSamples] = xr + xi * I; // first receiver + index += 6; + for (j = 0; j < quisk_multirx_count; j++) { // multirx receivers + xi = buf[index ] << 24 | buf[index + 1] << 16 | buf[index + 2] << 8; + xr = buf[index + 3] << 24 | buf[index + 4] << 16 | buf[index + 5] << 8; + c = xr + xi * I; + multirx_cSamples[j][nSamples] = c; + if (multirx_fft_data[j].index < multirx_fft_width) + multirx_fft_data[j].samples[multirx_fft_data[j].index++] = c; + index += 6; + } + nSamples++; + index += 2; + } + } + } + if ((quisk_pc_to_hermes[3] >> 3 & 0x7) != quisk_multirx_count && // change in number of receivers + ( ! quisk_multirx_count || multirx_fft_next_state == 2)) { // wait until the current FFT is finished + quisk_multirx_state = 0; // Do not change receiver count without stopping Hermes and restarting + } + if (multirx_fft_next_state == 2) { // previous FFT is done + if (++multirx_fft_next_index >= quisk_multirx_count) + multirx_fft_next_index = 0; + multirx_fft_next_state = 0; + } + if (quisk_multirx_count && multirx_fft_next_state == 0 && multirx_fft_data[multirx_fft_next_index].index >= multirx_fft_width) { // FFT is read to run + memcpy(multirx_fft_next_samples, multirx_fft_data[multirx_fft_next_index].samples, multirx_fft_width * sizeof(fftw_complex)); + multirx_fft_data[multirx_fft_next_index].index = 0; + multirx_fft_next_time = 1.0 / graph_refresh / quisk_multirx_count; + multirx_fft_next_state = 1; // this FFT is ready to run + } + return nSamples; +} + +static int read_rx_udp17(complex double * cSamples0) // Read samples from UDP +{ // Size of complex sample array is SAMP_BUFFER_SIZE + ssize_t bytes; + unsigned char buf[1500]; // Maximum Ethernet is 1500 bytes. + static unsigned char seq0; // must be 8 bits + int i, n, nSamples0, xr, xi, index, want_samples, key_down; + complex double sample; + unsigned char * ptxr, * ptxi; + struct timeval tm_wait; + fft_data * ptFFT; + fd_set fds; + static int block_number=0; + // Data from the receiver is little-endian + + if ( ! rx_udp_gain_correct) { // correct for second stage CIC decimation JIM JIM + int dec; + dec = (int)(rx_udp_clock / 30.0 / fft_sample_rate + 0.5); + if ((dec / 3) * 3 == dec) // Decimation by a factor of 3 + rx_udp_gain_correct = 1.053497942; + else // Decimation by factors of two + rx_udp_gain_correct = 1.0; + //QuiskPrintf ("Gain %d %.8lf\n", dec, rx_udp_gain_correct); + } + if ( ! quisk_rx_udp_started) { // we never received any data + // send our return address until we receive UDP blocks + tm_wait.tv_sec = 0; + tm_wait.tv_usec = 5000; + FD_ZERO (&fds); + FD_SET (rx_udp_socket, &fds); + if (select (rx_udp_socket + 1, &fds, NULL, NULL, &tm_wait) == 1) { // see if data is available + bytes = recv(rx_udp_socket, (char *)buf, 1500, 0); // throw away the first block + seq0 = buf[0] + 1; // Next expected sequence number + quisk_rx_udp_started = 1; +#if DEBUG_IO || DEBUG + QuiskPrintf("Udp data started\n"); +#endif + } + else { // send our return address to the sample source + buf[0] = buf[1] = 0x72; // UDP command "register return address" + send(rx_udp_socket, (char *)buf, 2, 0); + return 0; + } + } + nSamples0 = 0; + want_samples = (int)(quisk_sound_state.data_poll_usec * 1e-6 * quisk_sound_state.sample_rate + 0.5); + key_down = quisk_is_key_down(); + while (nSamples0 < want_samples) { // read several UDP blocks + tm_wait.tv_sec = 0; + tm_wait.tv_usec = 100000; // Linux seems to have problems with very small time intervals + FD_ZERO (&fds); + FD_SET (rx_udp_socket, &fds); + i = select (rx_udp_socket + 1, &fds, NULL, NULL, &tm_wait); + if (i == 1) + ; + else if (i == 0) { +#if DEBUG_IO || DEBUG + QuiskPrintf("Udp socket timeout\n"); +#endif + return 0; + } + else { +#if DEBUG_IO || DEBUG + QuiskPrintf("Udp select error %d\n", i); +#endif + return 0; + } + bytes = recv(rx_udp_socket, (char *)buf, 1500, 0); // blocking read + if (bytes != RX_UDP_SIZE) { // Known size of sample block + quisk_sound_state.read_error++; +#if DEBUG_IO || DEBUG + QuiskPrintf("read_rx_udp: Bad block size\n"); +#endif + continue; + } + // buf[0] is the sequence number + // buf[1] is the status: + // bit 0: key up/down state + // bit 1: set for ADC overrange (clip) + if (buf[0] != seq0) { +#if DEBUG_IO || DEBUG + QuiskPrintf("read_rx_udp: Bad sequence want %3d got %3d\n", + (unsigned int)seq0, (unsigned int)buf[0]); +#endif + quisk_sound_state.read_error++; + } + seq0 = buf[0] + 1; // Next expected sequence number + if (buf[1] & 0x02) // bit one is ADC overrange + quisk_sound_state.overrange++; + index = 2; + ptxr = (unsigned char *)&xr; + ptxi = (unsigned char *)ξ + // convert 24-bit samples to 32-bit samples; int must be 32 bits. + while (index < bytes) { + if (is_little_endian) { + xr = xi = 0; + memcpy (ptxr + 1, buf + index, 3); + index += 3; + memcpy (ptxi + 1, buf + index, 3); + index += 3; + sample = (xr + xi * I) * rx_udp_gain_correct; + } + else { // big-endian + *(ptxr ) = buf[index + 2]; + *(ptxr + 1) = buf[index + 1]; + *(ptxr + 2) = buf[index ]; + *(ptxr + 3) = 0; + index += 3; + *(ptxi ) = buf[index + 2]; + *(ptxi + 1) = buf[index + 1]; + *(ptxi + 2) = buf[index ]; + *(ptxi + 3) = 0; + index += 3; + sample = (xr + xi * I) * rx_udp_gain_correct; + } + if (xr & 0x100) { // channel 1 + if (quisk_invert_spectrum) // Invert spectrum + sample = conj(sample); + // Put samples into the fft input array. + ptFFT = fft_data_array + fft_data_index; + if ( ! (xi & 0x100)) { // zero marker for start of first block + if (ptFFT->index != 0) { + //QuiskPrintf("Resync block\n"); + fft_error++; + ptFFT->index = 0; + } + ptFFT->block = block_number = 0; + } + else if (ptFFT->index == 0) { + if (scan_blocks) { + if (++block_number < scan_blocks) + ptFFT->block = block_number; + else + ptFFT->block = block_number = 0; + } + else { + ptFFT->block = block_number = 0; + } + if (scan_blocks && block_number >= scan_blocks) + QuiskPrintf("Bad block_number %d\n", block_number); + } + ptFFT->samples[ptFFT->index] = sample; + if ((quisk_isFDX || ! key_down) && ++(ptFFT->index) >= fft_size) { // check sample count + n = fft_data_index + 1; // next FFT data location + if (n >= FFT_ARRAY_SIZE) + n = 0; + if (fft_data_array[n].filled == 0) { // Is the next buffer empty? + fft_data_array[n].index = 0; + fft_data_array[n].block = 0; + fft_data_array[fft_data_index].filled = 1; // Mark the previous buffer ready. + fft_data_index = n; // Write samples into the new buffer. + ptFFT = fft_data_array + fft_data_index; + } + else { // no place to write samples + ptFFT->index = 0; + ptFFT->block = 0; + fft_error++; + } + } + } + else { // channel 0 + cSamples0[nSamples0++] = sample; + } + } + } + return nSamples0; +} + +static PyObject * open_rx_udp(PyObject * self, PyObject * args) +{ + const char * ip; + int port; + char buf[128]; + struct sockaddr_in Addr; + int recvsize; + +#if DEBUG_IO + int intbuf; +#ifdef MS_WINDOWS + int bufsize = sizeof(int); +#else + socklen_t bufsize = sizeof(int); +#endif +#endif + +#ifdef MS_WINDOWS + WORD wVersionRequested; + WSADATA wsaData; +#endif + + if (!PyArg_ParseTuple (args, "si", &ip, &port)) + return NULL; +#ifdef MS_WINDOWS + wVersionRequested = MAKEWORD(2, 2); + if (WSAStartup(wVersionRequested, &wsaData) != 0) { + sprintf(buf, "Failed to initialize Winsock (WSAStartup)"); + return PyString_FromString(buf); + } + else { + cleanupWSA = 1; + } +#endif +#if DEBUG_IO + QuiskPrintf("open_rx_udp to IP %s port 0x%X\n", ip, port); +#endif + quisk_using_udp = 1; + rx_udp_socket = socket(PF_INET, SOCK_DGRAM, 0); + if (rx_udp_socket != INVALID_SOCKET) { + recvsize = 256000; + setsockopt(rx_udp_socket, SOL_SOCKET, SO_RCVBUF, (char *)&recvsize, sizeof(recvsize)); + memset(&Addr, 0, sizeof(Addr)); + Addr.sin_family = AF_INET; + Addr.sin_port = htons(port); +#ifdef MS_WINDOWS + Addr.sin_addr.S_un.S_addr = inet_addr(ip); +#else + inet_aton(ip, &Addr.sin_addr); +#endif + if (connect(rx_udp_socket, (const struct sockaddr *)&Addr, sizeof(Addr)) != 0) { + shutdown(rx_udp_socket, QUISK_SHUT_BOTH); + close(rx_udp_socket); + rx_udp_socket = INVALID_SOCKET; + sprintf(buf, "Failed to connect to UDP %s port 0x%X", ip, port); + } + else { + sprintf(buf, "Capture from UDP %s port 0x%X", ip, port); + if (quisk_use_rx_udp == 17) + quisk_sample_source(NULL, close_udp, read_rx_udp17); + else if (quisk_use_rx_udp == 10) { + quisk_sample_source(NULL, close_udp10, read_rx_udp10); + init_bandscope(); + } + else + quisk_sample_source(NULL, close_udp, quisk_read_rx_udp); +#if DEBUG_IO + if (getsockopt(rx_udp_socket, SOL_SOCKET, SO_RCVBUF, (char *)&intbuf, &bufsize) == 0) + QuiskPrintf("UDP socket receive buffer size %d\n", intbuf); + else + QuiskPrintf ("Failure SO_RCVBUF\n"); +#endif + } + } + else { + sprintf(buf, "Failed to open socket"); + } + return PyString_FromString(buf); +} + +static PyObject * open_sound(PyObject * self, PyObject * args) +{ + int rate; + char * mip; + + if (!PyArg_ParseTuple (args, "iiisiiiidi", + &rate, + &quisk_sound_state.data_poll_usec, + &quisk_sound_state.latency_millisecs, + &mip, + &quisk_sound_state.tx_audio_port, + &quisk_sound_state.mic_sample_rate, + &quisk_sound_state.mic_channel_I, + &quisk_sound_state.mic_channel_Q, + &quisk_sound_state.mic_out_volume, + &quisk_sound_state.mic_playback_rate + )) + return NULL; + +#if SAMPLES_FROM_FILE == 1 + QuiskWavWriteOpen(&hWav, "band.wav", 3, 2, 4, 48000, 1E3 / CLIP32); +#elif SAMPLES_FROM_FILE == 2 + QuiskWavReadOpen(&hWav, "band.wav", 3, 2, 4, 48000, CLIP32 / 1E6); +#endif + quisk_sound_state.playback_rate = QuiskGetConfigInt("playback_rate", 48000); + quisk_mic_preemphasis = QuiskGetConfigDouble("mic_preemphasis", 0.6); + //if (quisk_mic_preemphasis < 0.0 || quisk_mic_preemphasis > 1.0) + // quisk_mic_preemphasis = 1.0; + quisk_mic_clip = QuiskGetConfigDouble("mic_clip", 3.0); + agc_release_time = QuiskGetConfigDouble("agc_release_time", 1.0); + strMcpy(quisk_sound_state.mic_ip, mip, IP_SIZE); + strMcpy(quisk_sound_state.IQ_server, QuiskGetConfigString("IQ_Server_IP", ""), IP_SIZE); + quisk_sound_state.verbose_sound = quisk_sound_state.verbose_pulse = QuiskGetConfigInt("pulse_audio_verbose_output", 0); + fft_error = 0; + quisk_open_sound(); + quisk_open_mic(); + return get_state(NULL, NULL); +} + +static void configure_sound_thread(int job) // called from the sound thread except for job == 1 +{ +#ifdef MS_WINDOWS + DWORD taskIndex; + TIMECAPS tcaps; + static UINT timer_msec; // timer resolution in milliseconds; + + switch (job) { + case 0: // start sound thread +#if 0 + if (SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS) == 0) + if (quisk_sound_state.verbose_sound) + QuiskPrintf("Failed to set class priority\n"); + if (SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST) == 0) + if (quisk_sound_state.verbose_sound) + QuiskPrintf("Failed to set thread priority\n"); +#endif + taskIndex = 0; + if (AvSetMmThreadCharacteristics(TEXT("Pro Audio"), &taskIndex) == 0 && quisk_sound_state.verbose_sound) + QuiskPrintf("Failed to set sound thread to Pro Audio\n"); + timer_msec = 5; + if (timeGetDevCaps(&tcaps, sizeof(TIMECAPS)) == MMSYSERR_NOERROR) { + if (timer_msec < tcaps.wPeriodMin) + timer_msec = tcaps.wPeriodMin; + else if (timer_msec > tcaps.wPeriodMax) + timer_msec = tcaps.wPeriodMax; + } + if (quisk_sound_state.verbose_sound) + QuiskPrintf("Set Windows timer resolution to %u milliseconds\n", timer_msec); + if (timeBeginPeriod(timer_msec) != TIMERR_NOERROR && quisk_sound_state.verbose_sound) + QuiskPrintf ("Failed to set timer resolution to %u\n", timer_msec); + break; + case 1: // change rxMode + break; + case 2: // stop sound thread + if (timeEndPeriod(timer_msec) != TIMERR_NOERROR && quisk_sound_state.verbose_sound) + QuiskPrintf ("Failed to clear timer resolution\n"); + break; + } +#endif +} + +static PyObject * AppStatus(PyObject * self, PyObject * args) +{ + int status; + + if (!PyArg_ParseTuple (args, "i", &status)) + return NULL; +#ifdef MS_WINDOWS + if (status == 1) { // App is starting + // Initialize the critical section one time only. + InitializeCriticalSectionAndSpinCount(&QuiskCriticalSection, 0x00000400); + } + else if (status == 0) { + // Release resources used by the critical section object. + DeleteCriticalSection(&QuiskCriticalSection); + + } +#endif + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * GetQuiskPrintf(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; +#ifdef MS_WINDOWS + return QuiskPrintf(NULL); +#else + Py_INCREF (Py_None); + return Py_None; +#endif +} + +static PyObject * close_sound(PyObject * self, PyObject * args) // called from the sound thread +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + quisk_close_mic(); + quisk_close_sound(); +#if SAMPLES_FROM_FILE + QuiskWavClose(&hWav); +#endif + configure_sound_thread(2); + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * change_scan(PyObject * self, PyObject * args) // Called from GUI thread +{ // Change to a new FFT rate + + if (!PyArg_ParseTuple (args, "iidii", &scan_blocks, &scan_sample_rate, &scan_valid, &scan_vfo0, &scan_deltaf)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * change_rates(PyObject * self, PyObject * args) // Called from GUI thread +{ // Change to new sample rates + + multiple_sample_rates = 1; + if (!PyArg_ParseTuple (args, "iiii", &quisk_sound_state.sample_rate, &vfo_audio, &fft_sample_rate, &vfo_screen)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * change_rate(PyObject * self, PyObject * args) // Called from GUI thread +{ // Change to a new sample rate + int rate, avg; + + if (!PyArg_ParseTuple (args, "ii", &rate, &avg)) + return NULL; + if (multiple_sample_rates) { + fft_sample_rate = rate; + } + else { + quisk_sound_state.sample_rate = rate; + fft_sample_rate = rate; + } + rx_udp_gain_correct = 0; // re-calculate JIM + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * read_sound(PyObject * self, PyObject * args) +{ + int n; + + if (!PyArg_ParseTuple (args, "")) + return NULL; +Py_BEGIN_ALLOW_THREADS + if (quisk_close_file_play) { + quisk_close_file_play = 0; + wav_files_close(); + } + n = quisk_read_sound(); +Py_END_ALLOW_THREADS + return PyInt_FromLong(n); +} + +static PyObject * start_sound(PyObject * self, PyObject * args) // called from the sound thread +{ + + if (!PyArg_ParseTuple (args, "")) + return NULL; + configure_sound_thread(0); + configure_sound_thread(1); + quisk_start_sound(); + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * mixer_set(PyObject * self, PyObject * args) +{ + char * card_name; + int numid; + PyObject * value; + char err_msg[QUISK_SC_SIZE]; + + if (!PyArg_ParseTuple (args, "siO", &card_name, &numid, &value)) + return NULL; + + quisk_alsa_mixer_set(card_name, numid, value, err_msg, QUISK_SC_SIZE); + return PyString_FromString(err_msg); +} + +static PyObject * pc_to_hermes(PyObject * self, PyObject * args) +{ + PyObject * byteArray; + + if (!PyArg_ParseTuple (args, "O", &byteArray)) + return NULL; + if ( ! PyByteArray_Check(byteArray)) { + PyErr_SetString (QuiskError, "Object is not a bytearray."); + return NULL; + } + if (PyByteArray_Size(byteArray) != 17 * 4) { + PyErr_SetString (QuiskError, "Bytearray size must be 17 * 4."); + return NULL; + } + memmove(quisk_pc_to_hermes, PyByteArray_AsString(byteArray), 17 * 4); + Py_INCREF (Py_None); + return Py_None; +} + +// Changes for HermesLite v2 thanks to Steve, KF7O +static PyObject * pc_to_hermeslite_writequeue(PyObject * self, PyObject * args) +{ + PyObject * byteArray; + + if (!PyArg_ParseTuple (args, "O", &byteArray)) + return NULL; + if ( ! PyByteArray_Check(byteArray)) { + PyErr_SetString (QuiskError, "Object is not a bytearray."); + return NULL; + } + if (PyByteArray_Size(byteArray) != 5) { + PyErr_SetString (QuiskError, "Bytearray size must be 5."); + return NULL; + } + memmove(quisk_hermeslite_writequeue, PyByteArray_AsString(byteArray), 5); + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_hermeslite_writepointer(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "I", &quisk_hermeslite_writepointer)) + return NULL; + if (quisk_hermeslite_writepointer > 4 || quisk_hermeslite_writepointer < 0) { + PyErr_SetString (QuiskError, "Hermeslite writepointer must be >=0 and <=4."); + return NULL; + } + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * get_hermeslite_writepointer(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + return Py_BuildValue("I",quisk_hermeslite_writepointer); +} + +static PyObject * get_hermeslite_response(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + return PyByteArray_FromStringAndSize((char *)quisk_hermeslite_response, 5); +} + +static PyObject * clear_hermeslite_response(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + memset(quisk_hermeslite_response, 0, 5*sizeof(char)); + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * hermes_to_pc(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + return PyByteArray_FromStringAndSize((char *)quisk_hermes_to_pc, 5 * 4); +} + +static PyObject * set_hermes_id(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "ii", &quisk_hermes_code_version, &quisk_hermes_board_id)) + return NULL; + switch(quisk_hermes_board_id) { + default: + case 3: // Angelia and Odyssey-2 + bandscopeBlockCount = 32; + break; + case 6: // Hermes Lite + bandscopeBlockCount = 4; + break; + } + bandscope_size = bandscopeBlockCount * 512; + Py_INCREF (Py_None); + return Py_None; +} + +#ifdef MS_WINDOWS +static const char * Win_NtoA(unsigned long addr) +{ + static char buf32[32]; + + if (addr > 0) + snprintf(buf32, 32, "%li.%li.%li.%li", (addr>>24)&0xFF, (addr>>16)&0xFF, (addr>>8)&0xFF, (addr>>0)&0xFF); + else + buf32[0] = 0; + return buf32; +} + +#else +static const char * Lin_NtoA(struct sockaddr * a) +{ + static char buf32[32]; + unsigned long addr; + + if (a && (addr = ntohl(((struct sockaddr_in *)a)->sin_addr.s_addr)) > 0) + snprintf(buf32, 32, "%li.%li.%li.%li", (addr>>24)&0xFF, (addr>>16)&0xFF, (addr>>8)&0xFF, (addr>>0)&0xFF); + else + buf32[0] = 0; + return buf32; +} +#endif + +static PyObject * ip_interfaces(PyObject * self, PyObject * args) +{ +#ifdef MS_WINDOWS + int i; + MIB_IPADDRTABLE * ipTable = NULL; + IP_ADAPTER_INFO * pAdapterInfo; + PyObject * pylist, * tup; + MIB_IPADDRROW row; + ULONG bufLen; + DWORD ipRet, apRet; + const char * name; + unsigned long ipAddr, netmask, baddr; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + pylist = PyList_New(0); + bufLen = 0; + for (i=0; i<5; i++) { + ipRet = GetIpAddrTable(ipTable, &bufLen, 0); + if (ipRet == ERROR_INSUFFICIENT_BUFFER) { + free(ipTable); // in case we had previously allocated it + ipTable = (MIB_IPADDRTABLE *) malloc(bufLen); + } + else if (ipRet == NO_ERROR) + break; + else { + free(ipTable); + ipTable = NULL; + break; + } + } + + if (ipTable) { + pAdapterInfo = NULL; + bufLen = 0; + for (i=0; i<5; i++) { + apRet = GetAdaptersInfo(pAdapterInfo, &bufLen); + if (apRet == ERROR_BUFFER_OVERFLOW) { + free(pAdapterInfo); // in case we had previously allocated it + pAdapterInfo = (IP_ADAPTER_INFO *) malloc(bufLen); + } + else if (apRet == ERROR_SUCCESS) + break; + else { + free(pAdapterInfo); + pAdapterInfo = NULL; + break; + } + } + + for (i=0; idwNumEntries; i++) { + row = ipTable->table[i]; + // Now lookup the appropriate adaptor-name in the pAdaptorInfos, if we can find it + name = NULL; + if (pAdapterInfo) { + IP_ADAPTER_INFO * next = pAdapterInfo; + while((next)&&(name==NULL)) { + IP_ADDR_STRING * ipAddr = &next->IpAddressList; + while(ipAddr) { + if (inet_addr(ipAddr->IpAddress.String) == row.dwAddr) { + name = next->AdapterName; + break; + } + ipAddr = ipAddr->Next; + } + next = next->Next; + } + } + ipAddr = ntohl(row.dwAddr); + netmask = ntohl(row.dwMask); + baddr = ipAddr & netmask; + if (row.dwBCastAddr) + baddr |= ~netmask; + tup = PyTuple_New(4); + if (name == NULL) + PyTuple_SetItem(tup, 0, PyString_FromString("unnamed")); + else + PyTuple_SetItem(tup, 0, PyString_FromString(name)); + PyTuple_SetItem(tup, 1, PyString_FromString(Win_NtoA(ipAddr))); + PyTuple_SetItem(tup, 2, PyString_FromString(Win_NtoA(netmask))); + PyTuple_SetItem(tup, 3, PyString_FromString(Win_NtoA(baddr))); + PyList_Append(pylist, tup); + Py_DECREF(tup); + } + free(pAdapterInfo); + free(ipTable); + } +#else + PyObject * pylist, * tup; + struct ifaddrs * ifap, * p; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + pylist = PyList_New(0); + if (getifaddrs(&ifap) == 0) { + p = ifap; + while(p) { + if ((p->ifa_addr) && p->ifa_addr->sa_family == AF_INET) { + tup = PyTuple_New(4); + PyTuple_SetItem(tup, 0, PyString_FromString(p->ifa_name)); + PyTuple_SetItem(tup, 1, PyString_FromString(Lin_NtoA(p->ifa_addr))); + PyTuple_SetItem(tup, 2, PyString_FromString(Lin_NtoA(p->ifa_netmask))); + PyTuple_SetItem(tup, 3, PyString_FromString(Lin_NtoA(p->ifa_broadaddr))); + PyList_Append(pylist, tup); + Py_DECREF(tup); + } + p = p->ifa_next; + } + freeifaddrs(ifap); + } +#endif + return pylist; +} + +static PyObject * invert_spectrum(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "i", &quisk_invert_spectrum)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_agc(PyObject * self, PyObject * args) +{ /* Change the AGC level */ + if (!PyArg_ParseTuple (args, "d", &agcReleaseGain)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_filters(PyObject * self, PyObject * args) +{ // Enter the coefficients of the I and Q digital filters. The storage for + // filters is not malloc'd because filters may be changed while being used. + // Multiple filters are available at nFilter. + PyObject * filterI, * filterQ; + int i, size, nFilter, bw, start_offset; + PyObject * obj; + char buf98[98]; + + if (!PyArg_ParseTuple (args, "OOiii", &filterI, &filterQ, &bw, &start_offset, &nFilter)) + return NULL; + if (PySequence_Check(filterI) != 1) { + PyErr_SetString (QuiskError, "Filter I is not a sequence"); + return NULL; + } + if (PySequence_Check(filterQ) != 1) { + PyErr_SetString (QuiskError, "Filter Q is not a sequence"); + return NULL; + } + size = PySequence_Size(filterI); + if (size != PySequence_Size(filterQ)) { + PyErr_SetString (QuiskError, "The size of filters I and Q must be equal"); + return NULL; + } + if (size >= MAX_FILTER_SIZE) { + snprintf(buf98, 98, "Filter size must be less than %d", MAX_FILTER_SIZE); + PyErr_SetString (QuiskError, buf98); + return NULL; + } + filter_bandwidth[nFilter] = bw; + if (nFilter == 0) + filter_start_offset = start_offset; + for (i = 0; i < size; i++) { + obj = PySequence_GetItem(filterI, i); + cFilterI[nFilter][i] = PyFloat_AsDouble(obj); + Py_XDECREF(obj); + obj = PySequence_GetItem(filterQ, i); + cFilterQ[nFilter][i] = PyFloat_AsDouble(obj); + Py_XDECREF(obj); + } + sizeFilter = size; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_auto_notch(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "i", &quisk_auto_notch)) + return NULL; + dAutoNotch(NULL, 0, 0, 0); + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_noise_blanker(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "i", &quisk_noise_blanker)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_enable_bandscope(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "i", &enable_bandscope)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_rx_mode(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "i", &rxMode)) + return NULL; + quisk_set_tx_mode(); + quisk_set_play_state(); + configure_sound_thread(1); + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_spot_level(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "i", &quiskSpotLevel)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_imd_level(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "i", &quiskImdLevel)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_mic_out_volume(PyObject * self, PyObject * args) +{ + int level; + + if (!PyArg_ParseTuple (args, "i", &level)) + return NULL; + quisk_sound_state.mic_out_volume = level / 100.0; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * ImmediateChange(PyObject * self, PyObject * args) // called from the GUI thread +{ + char * name; + + if (!PyArg_ParseTuple (args, "s", &name)) + return NULL; + if ( ! strcmp(name, "keyupDelay")) { + quisk_sound_state.quiskKeyupDelay = QuiskGetConfigInt(name, 23); + } + else if ( ! strcmp(name, "cwTone")) { + quisk_sidetoneFreq = QuiskGetConfigInt(name, 700); + } + else if ( ! strcmp(name, "pulse_audio_verbose_output")) { + quisk_sound_state.verbose_sound = quisk_sound_state.verbose_pulse = QuiskGetConfigInt(name, 0); + } + else if ( ! strcmp(name, "start_cw_delay")) { + quisk_start_cw_delay = QuiskGetConfigInt(name, 15); + if (quisk_start_cw_delay < 0) + quisk_start_cw_delay = 0; + else if (quisk_start_cw_delay > START_CW_DELAY_MAX) + quisk_start_cw_delay = START_CW_DELAY_MAX; + } + else if ( ! strcmp(name, "start_ssb_delay")) { + quisk_start_ssb_delay = QuiskGetConfigInt(name, 100); + } + else if ( ! strcmp(name, "maximum_tx_secs")) { + maximum_tx_secs = QuiskGetConfigInt(name, 0); + } + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_split_rxtx(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "i", &split_rxtx)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_tune(PyObject * self, PyObject * args) +{ /* Change the tuning frequency */ + if (!PyArg_ParseTuple (args, "ii", &rx_tune_freq, &quisk_tx_tune_freq)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_sidetone(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "idii", &quisk_sidetoneCtrl, &quisk_sidetoneVolume, &rit_freq, &quisk_sound_state.quiskKeyupDelay)) + return NULL; + sidetonePhase = cexp((I * 2.0 * M_PI * abs(rit_freq)) / quisk_sound_state.playback_rate); + if (rxMode == CWL || rxMode == CWU) + dAutoNotch(NULL, 0, 0, 0); // for CW, changing the RIT affects autonotch + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_squelch(PyObject * self, PyObject * args) // Set level for FM squelch +{ + if (!PyArg_ParseTuple (args, "d", &squelch_level)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_ssb_squelch(PyObject * self, PyObject * args) // Set level for SSB squelch +{ + if (!PyArg_ParseTuple (args, "ii", &ssb_squelch_enabled, &ssb_squelch_level)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_kill_audio(PyObject * self, PyObject * args) +{ /* replace radio sound with silence */ + if (!PyArg_ParseTuple (args, "i", &kill_audio)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * tx_hold_state(PyObject * self, PyObject * args) +{ // Query or set the transmit hold state + int i; + + if (!PyArg_ParseTuple (args, "i", &i)) + return NULL; + if (i >= 0) // arg < 0 is a Query for the current value + quiskTxHoldState = i; + return PyInt_FromLong(quiskTxHoldState); +} + +static PyObject * set_transmit_mode(PyObject * self, PyObject * args) +{ /* Set the radio to transmit mode */ + if (!PyArg_ParseTuple (args, "i", &quisk_transmit_mode)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_volume(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "d", &quisk_audioVolume)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_ctcss(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "d", &quisk_ctcss_freq)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_key_down(PyObject * self, PyObject * args) +{ + int down; + + if (!PyArg_ParseTuple (args, "i", &down)) + return NULL; + quisk_set_key_down(down); + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_hardware_cwkey(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "i", &quisk_hardware_cwkey)) + return NULL; + if (quisk_hardware_cwkey != old_hardware_cwkey) { + old_hardware_cwkey = quisk_hardware_cwkey; + quisk_set_play_state(); + //if (quisk_hardware_cwkey) QuiskPrintTime("set hardware cwkey", 0); + } + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_remote_cwkey(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "i", &quisk_remote_cwkey)) + return NULL; + if (quisk_remote_cwkey != old_remote_cwkey) { + old_remote_cwkey = quisk_remote_cwkey; + quisk_set_play_state(); + //if (quisk_remote_cwkey) QuiskPrintTime("set remote cwkey", 0); + } + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_PTT(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "i", &is_PTT_down)) + return NULL; + quisk_set_play_state(); + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_multirx_mode(PyObject * self, PyObject * args) +{ + int index, mode; + + if (!PyArg_ParseTuple (args, "ii", &index, &mode)) + return NULL; + if (index < QUISK_MAX_SUB_RECEIVERS) + multirx_mode[index] = mode; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_multirx_freq(PyObject * self, PyObject * args) +{ + int index, freq; + + if (!PyArg_ParseTuple (args, "ii", &index, &freq)) + return NULL; + if (index < QUISK_MAX_SUB_RECEIVERS) + multirx_freq[index] = freq; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_multirx_play_method(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "i", &multirx_play_method)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_multirx_play_channel(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "i", &multirx_play_channel)) + return NULL; + if (multirx_play_channel >= QUISK_MAX_SUB_RECEIVERS) + multirx_play_channel = -1; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * get_multirx_graph(PyObject * self, PyObject * args) // Called by the GUI thread +{ + int i, j, k; + double d1, d2, scale; + static double * fft_window=NULL; // Window for FFT data + PyObject * retrn, * data; + static double time0=0; // time of last graph + + if (!PyArg_ParseTuple (args, "")) + return NULL; + if ( ! fft_window) { + // Create the fft window + fft_window = (double *) malloc(sizeof(double) * multirx_fft_width); + for (i = 0, j = -multirx_fft_width / 2; i < multirx_fft_width; i++, j++) + fft_window[i] = 0.5 + 0.5 * cos(2. * M_PI * j / multirx_fft_width); // Hanning + } + retrn = PyTuple_New(2); + if (multirx_fft_next_state == 1 && QuiskTimeSec() - time0 >= multirx_fft_next_time) { + time0 = QuiskTimeSec(); + // The FFT is ready to run. Calculate FFT. + for (i = 0; i < multirx_fft_width; i++) // multiply by window + multirx_fft_next_samples[i] *= fft_window[i]; + fftw_execute(multirx_fft_next_plan); + // Average the fft data into the graph in order of frequency + data = PyTuple_New(multirx_data_width); + scale = log10(multirx_fft_width) + 31.0 * log10(2.0); + scale *= 20.0; + j = MULTIRX_FFT_MULT; + k = 0; + d1 = 0; + for (i = multirx_fft_width / 2; i < multirx_fft_width; i++) { // Negative frequencies + d1 += cabs(multirx_fft_next_samples[i]); + if (--j == 0) { + d2 = 20.0 * log10(d1) - scale; + if (d2 < -200) + d2 = -200; + PyTuple_SetItem(data, k++, PyFloat_FromDouble(d2)); + d1 = 0; + j = MULTIRX_FFT_MULT; + } + } + for (i = 0; i < multirx_fft_width / 2; i++) { // Positive frequencies + d1 += cabs(multirx_fft_next_samples[i]); + if (--j == 0) { + d2 = 20.0 * log10(d1) - scale; + if (d2 < -200) + d2 = -200; + PyTuple_SetItem(data, k++, PyFloat_FromDouble(d2)); + d1 = 0; + j = MULTIRX_FFT_MULT; + } + } + PyTuple_SetItem(retrn, 0, data); + PyTuple_SetItem(retrn, 1, PyInt_FromLong(multirx_fft_next_index)); + multirx_fft_next_state = 2; // This FFT is done. + } + else { + data = PyTuple_New(0); + PyTuple_SetItem(retrn, 0, data); + PyTuple_SetItem(retrn, 1, PyInt_FromLong(-1)); + } + return retrn; +} + +void copy2pixels(double * pixels, int n_pixels, double * fft, int fft_size, double zoom, double deltaf, double rate) +{ + int i, j, j1, j2; + double f1, d1, d2, sample; + + f1 = deltaf + rate / 2.0 * (1.0 - zoom); // frequency at left of graph + for (i = 0; i < n_pixels; i++) { // for each pixel + // freq = f1 + pixel / n_pixels * zoom * rate = rate * fft_index / fft_size + d1 = fft_size / rate * (f1 + (double)i / n_pixels * zoom * rate); + d2 = fft_size / rate * (f1 + (double)(i + 1) / n_pixels * zoom * rate); + j1 = floor(d1); + j2 = floor(d2); + if (j1 == j2) { + sample = (d2 - d1) * fft[j1]; + } + else { + sample = (j1 + 1 - d1) * fft[j1]; + for (j = j1 + 1; j < j2; j++) + sample += fft[j]; + sample += (d2 - j2) * fft[j2]; + } + pixels[i] = sample; + } +} + +static PyObject * get_bandscope(PyObject * self, PyObject * args) // Called by the GUI thread +{ + int i, L, clock; + double zoom, deltaf, rate; + static int fft_count = 0; + static double the_max = 0; + static double time0=0; // time of last graph + double d1, sample, frac, scale; + PyObject * tuple2; + + if (!PyArg_ParseTuple (args, "idd", &clock, &zoom, &deltaf)) + return NULL; + + if (bandscopeState == 99 && bandscopePlan) { // bandscope samples are ready + for (i = 0; i < bandscope_size; i++) { + d1 = fabs(bandscopeSamples[i]); + if (d1 > the_max) + the_max = d1; + bandscopeSamples[i] *= bandscopeWindow[i]; // multiply by window + } + fftw_execute(bandscopePlan); // Calculate forward FFT + // The return FFT has length bandscope_size / 2 + 1 + L = bandscope_size / 2 + 1; + for (i = 0; i < L; i++) + bandscopeAverage[i] += cabs(bandscopeFFT[i]); + bandscopeState = 0; + fft_count++; + if (QuiskTimeSec() - time0 >= 1.0 / graph_refresh) { // return FFT data + bandscopeAverage[L] = 0.0; // in case we run off the end + // Average the return FFT into the data width + tuple2 = PyTuple_New(graph_width); + frac = (double)L / graph_width; + scale = 1.0 / frac / fft_count / bandscope_size; + rate = clock / 2.0; + copy2pixels(bandscopePixels, graph_width, bandscopeAverage, L, zoom, deltaf, rate); + for (i = 0; i < graph_width; i++) { // for each pixel + sample = bandscopePixels[i] * scale; + if (sample <= 1E-10) + sample = -200.0; + else + sample = 20.0 * log10(sample); + PyTuple_SetItem(tuple2, i, PyFloat_FromDouble(sample)); + } + fft_count = 0; + time0 = QuiskTimeSec(); + hermes_adc_level = the_max; + the_max = 0; + for (i = 0; i < L; i++) + bandscopeAverage[i] = 0; + return tuple2; + } + } + Py_INCREF(Py_None); // No data yet + return Py_None; +} + +static PyObject * get_graph(PyObject * self, PyObject * args) // Called by the GUI thread +{ + int i, j, k, m, n, index, ffts, ii, mm, m0, deltam; + fft_data * ptFft; + PyObject * tuple2; + double d1, d2, scale, smeter_scale, zoom, deltaf; + complex double c; + static double meter = 0; // RMS s-meter + static int job = 1; // job==0 return raw data ; 1 return FFT ; 2 delete FFT data + static double * fft_avg=NULL; // Array to average the FFT + static double * fft_tmp; + static int count_fft=0; // how many fft's have occurred (for average) + static double time0=0; // time of last graph + static double time_send_graph; // time of the last send_graph_data() + + if (!PyArg_ParseTuple (args, "idd", &k, &zoom, &deltaf)) + return NULL; + if (k != job) { // change in data return type; re-initialize + job = k; + count_fft = 0; + } + if ( ! fft_avg) { + fft_avg = (double *) malloc(sizeof(double) * fft_size); + fft_tmp = (double *) malloc(sizeof(double) * fft_size); + for (i = 0; i < fft_size; i++) + fft_avg[i] = 0; + } + if (remote_control_head) { + n = receive_graph_data(fft_avg); + if (n == data_width) { + tuple2 = PyTuple_New(data_width); + for (i = 0; i < data_width; i++) + PyTuple_SetItem(tuple2, i, PyFloat_FromDouble(fft_avg[i])); + return tuple2; + } + job = 2; + } + if (remote_control_slave) { + if (QuiskTimeSec() - time_send_graph > 1.0) { + time_send_graph = QuiskTimeSec(); + send_graph_data(NULL, 0, 0.0, 0.0, 0, 0.0); + } + } + // Process all FFTs that are ready to run. + index = fft_data_index; // oldest data first - FIFO + for (ffts = 0; ffts < FFT_ARRAY_SIZE; ffts++) { + if (++index >= FFT_ARRAY_SIZE) + index = 0; + if (fft_data_array[index].filled) + ptFft = fft_data_array + index; + else + continue; + if (scan_blocks && ptFft->block >= scan_blocks) { + //QuiskPrintf("Reject block %d\n", ptFft->block); + ptFft->filled = 0; + continue; + } + if (job == 0) { // return raw data, not FFT + tuple2 = PyTuple_New(data_width); + for (i = 0; i < data_width; i++) + PyTuple_SetItem(tuple2, i, + PyComplex_FromDoubles(creal(ptFft->samples[i]), cimag(ptFft->samples[i]))); + ptFft->filled = 0; + return tuple2; + } + if (job == 2) { // delete data + ptFft->filled = 0; + continue; + } + // Continue with FFT calculation + for (i = 0; i < fft_size; i++) // multiply by window + ptFft->samples[i] *= fft_window[i]; + fftw_execute_dft(quisk_fft_plan, ptFft->samples, ptFft->samples); // Calculate FFT + // Create RMS s-meter value at known bandwidth + // The pass band is (rx_tune_freq + filter_start_offset) to += bandwidth + // d1 is the tune frequency + // d2 is the number of FFT bins required for the bandwidth + // i is the starting bin number from - sample_rate / 2 to + sample_rate / 2 + d2 = (double)filter_bandwidth[0] * fft_size / fft_sample_rate; + if (scan_blocks) { // Use tx, not rx?? ERROR: + d1 = ((double)quisk_tx_tune_freq + vfo_screen - scan_vfo0 - scan_deltaf * ptFft->block) * fft_size / scan_sample_rate; + i = (int)(d1 - d2 / 2 + 0.5); + } + else + i = (int)((double)(rx_tune_freq + filter_start_offset) * fft_size / fft_sample_rate + 0.5); + n = (int)(floor(d2) + 0.01); // number of whole bins to add + if (i > - fft_size / 2 && i + n + 1 < fft_size / 2) { // too close to edge? + for (j = 0; j < n; i++, j++) { + if (i < 0) + c = ptFft->samples[fft_size + i]; // negative frequencies + else + c = ptFft->samples[i]; // positive frequencies + meter = meter + c * conj(c); // add square of amplitude + } + if (i < 0) // add fractional next bin + c = ptFft->samples[fft_size + i]; + else + c = ptFft->samples[i]; + meter = meter + c * conj(c) * (d2 - n); // fractional part of next bin + } + // Average the fft data into the graph in order of frequency + if (scan_blocks) { + if (ptFft->block == (scan_blocks - 1)) + count_fft++; + k = 0; + for (i = fft_size / 2; i < fft_size; i++) // Negative frequencies + fft_tmp[k++] = cabs(ptFft->samples[i]); + for (i = 0; i < fft_size / 2; i++) // Positive frequencies + fft_tmp[k++] = cabs(ptFft->samples[i]); + // Average this block into its correct position + m0 = (int)(fft_size * ((1.0 - scan_valid) / 2.0)); + deltam = (int)(fft_size * scan_valid / scan_blocks); + m = mm = m0 + ptFft->block * deltam; // target position + i = ii = (int)(fft_size * ((1.0 - scan_valid) / 2.0)); // start of valid data + for (j = 0; j < deltam; j++) { + d2 = 0; + for (n = 0; n < scan_blocks; n++) + d2 += fft_tmp[i++]; + fft_avg[m++] = d2; + } + //QuiskPrintf(" %d %.4lf At %5d to %5d place %5d to %5d for block %d\n", fft_size, scan_valid, mm, m, ii, i, ptFft->block); + } + else { + count_fft++; + k = 0; + for (i = fft_size / 2; i < fft_size; i++) // Negative frequencies + fft_avg[k++] += cabs(ptFft->samples[i]); + for (i = 0; i < fft_size / 2; i++) // Positive frequencies + fft_avg[k++] += cabs(ptFft->samples[i]); + } + ptFft->filled = 0; + if (count_fft > 0 && QuiskTimeSec() - time0 >= 1.0 / graph_refresh) { + // We have averaged enough fft's to return the graph data. + scale = 1.0 / 2147483647.0 / fft_size; + // scale = 1.0 / count_fft / fft_size; // Divide by sample count + // scale /= pow(2.0, 31); // Normalize to max == 1 + scale = log10(count_fft) + log10(fft_size) + 31.0 * log10(2.0); + scale *= 20.0; + if (remote_control_slave) // Send graph data to the control head + send_graph_data(fft_avg, fft_size, zoom, deltaf, fft_sample_rate, scale); + // Average the fft data of size fft_size into the size of data_width. + n = (int)(zoom * (double)fft_size / data_width + 0.5); + if (n < 1) + n = 1; + for (i = 0; i < data_width; i++) { // For each graph pixel + // find k, the starting index into the FFT data + k = (int)(fft_size * ( + deltaf / fft_sample_rate + zoom * ((double)i / data_width - 0.5) + 0.5) + 0.1); + d2 = 0.0; + for (j = 0; j < n; j++, k++) + if (k >= 0 && k < fft_size) + d2 += fft_avg[k]; + fft_avg[i] = d2; + } + smeter_scale = 1.0 / 2147483647.0 / fft_size; + Smeter = meter * smeter_scale * smeter_scale / count_fft; // record the new s-meter value + meter = 0; + if (Smeter > 1E-16) + Smeter = 10.0 * log10(Smeter); + else + Smeter = -160.0; + // This correction is for a -40 dB strong signal, and is caused by FFT leakage + // into adjacent bins. It is the amplitude that is spread out, not the squared amplitude. + Smeter += 4.25969; + tuple2 = PyTuple_New(data_width); + for (i = 0; i < data_width; i++) { + d2 = 20.0 * log10(fft_avg[i]) - scale; + if (d2 < -200) + d2 = -200; + else if (d2 > 0) + d2 = 0; + current_graph[i] = d2; // graph values are -200.0 to 0.0 + PyTuple_SetItem(tuple2, i, PyFloat_FromDouble(d2)); + } + for (i = 0; i < fft_size; i++) + fft_avg[i] = 0; + count_fft = 0; + time0 = time_send_graph = QuiskTimeSec(); + return tuple2; + } + } + Py_INCREF(Py_None); // No data yet + return Py_None; +} + +// These functions are used for the Waterfall display. +static PyObject * watfall_RgbData(PyObject * self, PyObject * args) // Called by the GUI thread +{ + int i, width, max_height, size; + Py_buffer red, green, blue; + PyObject * bytes; + struct watfall_t watfall; + struct watfall_row_t * row, * next; + + if (!PyArg_ParseTuple (args, "w*w*w*ii", &red, &green, &blue, &width, &max_height)) + return NULL; + + memcpy(watfall.red, red.buf, 256); + memcpy(watfall.green, green.buf, 256); + memcpy(watfall.blue, blue.buf, 256); + PyBuffer_Release(&red); + PyBuffer_Release(&green); + PyBuffer_Release(&blue); + watfall.width = width; + watfall.max_height = max_height; + // malloc space for the maximum number of rows + size = sizeof(struct watfall_row_t) + width * 3; // struct plus pixel data for the row + row = malloc(size); + memset(row, 0, size); + row->prior_row = NULL; + watfall.current_row = row; + next = NULL; + for (i = 1; i < max_height; i++) { + next = malloc(size); + memset(next, 0, size); + next->prior_row = row; + row->next_row = next; + row = next; + } + next->next_row = watfall.current_row; + watfall.current_row->prior_row = next; + bytes = PyByteArray_FromStringAndSize((const char *)&watfall, sizeof(watfall)); + return bytes; +} + +static PyObject * watfall_OnGraphData(PyObject * self, PyObject * args) // Called by the GUI thread +{ + int i, l, y_zero, y_scale, x_origin, size; + double yz, dB, gain; + uint8_t * pPixels; + Py_buffer rgb_data; + PyObject * db_list, * obj; + struct watfall_t * pWatfall; + struct watfall_row_t * pRow; + + if (!PyArg_ParseTuple (args, "w*Oiidi", &rgb_data, &db_list, &y_zero, &y_scale, &gain, &x_origin)) + return NULL; + if (PySequence_Check(db_list) != 1) { + PyErr_SetString (QuiskError, "List of dB data is not a sequence"); + return NULL; + } + + pWatfall = (struct watfall_t *)rgb_data.buf; + pWatfall->current_row = pWatfall->current_row->prior_row; + pRow = pWatfall->current_row; + // replace data in oldest row + pRow->x_origin = x_origin; + pPixels = pRow->pixels; + size = PySequence_Size(db_list); + if (size > pWatfall->width) + size = pWatfall->width; + yz = 40.0 + y_zero * 0.69; // -yz is the color center in dB + for (i = 0; i < size; i++) { + obj = PySequence_GetItem(db_list, i); + dB = PyFloat_AsDouble(obj); // x is -130 to 0, or so (dB) + Py_DECREF(obj); + l = (int)((dB - gain + yz) * (y_scale + 10) * 0.10 + 128); + if (l < 0) + l = 0; + else if(l > 255) + l = 255; + *pPixels++ = pWatfall->red[l]; + *pPixels++ = pWatfall->green[l]; + *pPixels++ = pWatfall->blue[l]; + } + for ( ; i < pWatfall->width; i++) { + *pPixels++ = 0; + *pPixels++ = 0; + *pPixels++ = 0; + } + PyBuffer_Release(&rgb_data); + Py_INCREF(Py_None); + return Py_None; +} + +static uint8_t * watfall_copy(uint8_t * dest, uint8_t * source, int x, int width) +{ // Copy the source to location x in the dest. The source and dest have size width. + if (x == 0) { + memcpy(dest, source, width); + } + else if (x >= width || x + width <= 0) { // no overlap + memset(dest, 0, width); + } + else if (x > 0) { + memset(dest, 0, x); + memcpy(dest + x, source, width - x); + } + else { + x = -x; + memcpy(dest, source + x, width - x); + memset(dest + width - x, 0, x); + } + return dest + width; +} + +static PyObject * watfall_GetPixels(PyObject * self, PyObject * args) // Called by the GUI thread +{ + int i, j, x, x_origin, width, width3, height; + Py_buffer rgb_data, pixels; + struct watfall_t * pWatfall; + struct watfall_row_t * pRow; + uint8_t * pDest; + + if (!PyArg_ParseTuple (args, "w*w*iii", &rgb_data, &pixels, &x_origin, &width, &height)) + return NULL; + + width3 = width * 3; + pDest = pixels.buf; + pWatfall = (struct watfall_t *)rgb_data.buf; + pRow = pWatfall->current_row; + if (waterfall_scroll_mode) { + for (j = 8; j > 1; j--) { + x = pRow->x_origin - x_origin; + x *= 3; + for (i = 0; i < j; i++) { // Copy the first rows multiple times + pDest = watfall_copy(pDest, pRow->pixels, x, width3); + height--; + } + pRow = pRow->next_row; + } + } + for (i = 0; i < height; i++) { + x = pRow->x_origin - x_origin; + x *= 3; + pDest = watfall_copy(pDest, pRow->pixels, x, width3); + pRow = pRow->next_row; + } + PyBuffer_Release(&rgb_data); + PyBuffer_Release(&pixels); + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject * get_filter(PyObject * self, PyObject * args) +{ + int i, j, k, n; + int freq, time; + PyObject * tuple2; + complex double cx; + double d2, scale, accI, accQ; + double * average, * bufI, * bufQ; + double phase, delta; + static fftw_complex * samples; + static fftw_plan plan; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + + // Create space for the fft of size data_width + samples = (fftw_complex *) fftw_malloc(sizeof(fftw_complex) * data_width); + plan = fftw_plan_dft_1d(data_width, samples, samples, FFTW_FORWARD, FFTW_MEASURE); + average = (double *) malloc(sizeof(double) * (data_width + sizeFilter)); + bufI = (double *) malloc(sizeof(double) * sizeFilter); + bufQ = (double *) malloc(sizeof(double) * sizeFilter); + + for (i = 0; i < data_width + sizeFilter; i++) + average[i] = 0.5; // Value for freq == 0 + for (freq = 1; freq < data_width / 2.0 - 10.0; freq++) { + delta = 2 * M_PI / data_width * freq; + phase = 0; + // generate some initial samples to fill the filter pipeline + for (time = 0; time < data_width + sizeFilter; time++) { + average[time] += cos(phase); // current sample + phase += delta; + if (phase > 2 * M_PI) + phase -= 2 * M_PI; + } + } + // now filter the signal + n = 0; + for (time = 0; time < data_width + sizeFilter; time++) { + d2 = average[time]; + bufI[n] = d2; + bufQ[n] = d2; + accI = accQ = 0; + j = n; + for (k = 0; k < sizeFilter; k++) { + accI += bufI[j] * cFilterI[0][k]; + accQ += bufQ[j] * cFilterQ[0][k]; + if (++j >= sizeFilter) + j = 0; + } + cx = accI + I * accQ; // Filter output + if (++n >= sizeFilter) + n = 0; + if (time >= sizeFilter) + samples[time - sizeFilter] = cx; + } + + for (i = 0; i < data_width; i++) // multiply by window + samples[i] *= fft_window[i]; + fftw_execute(plan); // Calculate FFT + // Normalize and convert to log10 + scale = 1. / data_width; + for (k = 0; k < data_width; k++) { + cx = samples[k]; + average[k] = cabs(cx) * scale; + if (average[k] <= 1e-7) // limit to -140 dB + average[k] = -7; + else + average[k] = log10(average[k]); + } + // Return the graph data + tuple2 = PyTuple_New(data_width); + i = 0; + // Negative frequencies: + for (k = data_width / 2; k < data_width; k++, i++) + PyTuple_SetItem(tuple2, i, PyFloat_FromDouble(20.0 * average[k])); + + // Positive frequencies: + for (k = 0; k < data_width / 2; k++, i++) + PyTuple_SetItem(tuple2, i, PyFloat_FromDouble(20.0 * average[k])); + + free(bufQ); + free(bufI); + free(average); + fftw_destroy_plan(plan); + fftw_free(samples); + + return tuple2; +} + +static PyObject * quisk_control_midi(PyObject * self, PyObject * args, PyObject * keywds) +{ +#ifdef QUISK_HAVE_ALSA + return quisk_alsa_control_midi(self, args, keywds); +#else + return quisk_wasapi_control_midi(self, args, keywds); +#endif +} + +static void measure_freq(complex double * cSamples, int nSamples, int srate) +{ + int i, k, center, ipeak; + double dmax, c3, freq; + complex double cBuffer[SAMP_BUFFER_SIZE]; + static int index = 0; // current index of samples + static int fft_size=12000; // size of fft data + static int fft_count=0; // number of ffts for the average + static fftw_complex * samples; // complex data for fft + static fftw_plan planA; // fft plan for fft + static double * fft_window; // window function + static double * fft_average; // average amplitudes + static struct quisk_cHB45Filter HalfBand1 = {NULL, 0, 0}; + static struct quisk_cHB45Filter HalfBand2 = {NULL, 0, 0}; + static struct quisk_cHB45Filter HalfBand3 = {NULL, 0, 0}; + + if ( ! cSamples) { // malloc new space and initialize + samples = (fftw_complex *) fftw_malloc(sizeof(fftw_complex) * fft_size); + planA = quisk_create_or_cache_fftw_plan_dft_1d(fft_size, samples, samples, FFTW_FORWARD, FFTW_MEASURE); + fft_window = (double *) malloc(sizeof(double) * (fft_size + 1)); + fft_average = (double *) malloc(sizeof(double) * fft_size); + memset(fft_average, 0, sizeof(double) * fft_size); + for (i = 0; i < fft_size; i++) // Hanning + fft_window[i] = 0.50 - 0.50 * cos(2. * M_PI * i / (fft_size - 1)); + return; + } + memcpy(cBuffer, cSamples, nSamples * sizeof(complex double)); // do not destroy cSamples + nSamples = quisk_cDecim2HB45(cBuffer, nSamples, &HalfBand1); + nSamples = quisk_cDecim2HB45(cBuffer, nSamples, &HalfBand2); + nSamples = quisk_cDecim2HB45(cBuffer, nSamples, &HalfBand3); + srate /= 8; // sample rate as decimated + for (i = 0; i < nSamples && index < fft_size; i++, index++) + samples[index] = cBuffer[i]; + if (index < fft_size) + return; // wait for a full array of samples + for (i = 0; i < fft_size; i++) // multiply by window + samples[i] *= fft_window[i]; + fftw_execute(planA); // Calculate FFT + index = 0; + fft_count++; + // Average the fft data into the graph in order of frequency + k = 0; + for (i = fft_size / 2; i < fft_size; i++) // Negative frequencies + fft_average[k++] += cabs(samples[i]); + for (i = 0; i < fft_size / 2; i++) // Positive frequencies + fft_average[k++] += cabs(samples[i]); + if (fft_count < measure_freq_mode / 2) + return; // continue with average + // time for a calculation + fft_count = 0; + dmax = 1.e-20; + ipeak = 0; + center = fft_size / 2 - rit_freq * fft_size / srate; + k = 500; // desired +/- half-bandwidth to search for a peak + k = k * fft_size / srate; // convert to index + for (i = center - k; i <= center + k; i++) { // search for a peak near the RX freq + if (fft_average[i] > dmax) { + dmax = fft_average[i]; + ipeak = i; + } + } + c3 = 1.36 * (fft_average[ipeak+1] - fft_average[ipeak - 1]) / (fft_average[ipeak-1] + fft_average[ipeak] + fft_average[ipeak+1]); + freq = srate * (2 * (ipeak + c3) - fft_size) / 2 / fft_size; + freq += rx_tune_freq; + //QuiskPrintf("freq %.0f rx_tune_freq %d vfo_screen %d vfo_audio %d\n", freq, rx_tune_freq, vfo_screen, vfo_audio); + // QuiskPrintf("\n%5d %.4lf %.2lf k=%d\n", ipeak, c3, freq, k); + measured_frequency = freq; + //for (i = ipeak - 10; i <= ipeak + 10 && i >= 0 && i < fft_size; i++) + // QuiskPrintf("%4d %12.5f\n", i, fft_average[i] / dmax); + memset(fft_average, 0, sizeof(double) * fft_size); +} + +static PyObject * Xdft(PyObject * pyseq, int inverse, int window) +{ // Native spectral order is 0 Hz to (Fs - 1). Change this to + // - (Fs - 1)/2 to + Fs/2. For even Fs==32, there are 15 negative + // frequencies, a zero, and 16 positive frequencies. For odd Fs==31, + // there are 15 negative and positive frequencies plus zero frequency. + // Note that zero frequency is always index (Fs - 1) / 2. + PyObject * obj; + int i, j, size; + static int fft_size = -1; // size of fft data + static fftw_complex * samples; // complex data for fft + static fftw_plan planF, planB; // fft plan for fftW + static double * fft_window; // window function + Py_complex pycx; // Python C complex value + + if (PySequence_Check(pyseq) != 1) { + PyErr_SetString (QuiskError, "DFT input data is not a sequence"); + return NULL; + } + size = PySequence_Size(pyseq); + if (size <= 0) + return PyTuple_New(0); + if (size != fft_size) { // Change in previous size; malloc new space + if (fft_size > 0) { + fftw_destroy_plan(planF); + fftw_destroy_plan(planB); + fftw_free(samples); + free (fft_window); + } + fft_size = size; // Create space for one fft + samples = (fftw_complex *) fftw_malloc(sizeof(fftw_complex) * fft_size); + planF = fftw_plan_dft_1d(fft_size, samples, samples, FFTW_FORWARD, FFTW_MEASURE); + planB = fftw_plan_dft_1d(fft_size, samples, samples, FFTW_BACKWARD, FFTW_MEASURE); + fft_window = (double *) malloc(sizeof(double) * (fft_size + 1)); + for (i = 0; i <= size/2; i++) { + if (1) // Blackman window + fft_window[i] = fft_window[size - i] = 0.42 + 0.50 * cos(2. * M_PI * i / size) + + 0.08 * cos(4. * M_PI * i / size); + else if (1) // Hamming + fft_window[i] = fft_window[size - i] = 0.54 + 0.46 * cos(2. * M_PI * i / size); + else // Hanning + fft_window[i] = fft_window[size - i] = 0.50 + 0.50 * cos(2. * M_PI * i / size); + } + } + j = (size - 1) / 2; // zero frequency in input + for (i = 0; i < size; i++) { + obj = PySequence_GetItem(pyseq, j); + if (PyComplex_Check(obj)) { + pycx = PyComplex_AsCComplex(obj); + } + else if (PyFloat_Check(obj)) { + pycx.real = PyFloat_AsDouble(obj); + pycx.imag = 0; + } + else if (PyInt_Check(obj)) { + pycx.real = PyInt_AsLong(obj); + pycx.imag = 0; + } + else { + Py_XDECREF(obj); + PyErr_SetString (QuiskError, "DFT input data is not a complex/float/int number"); + return NULL; + } + samples[i] = pycx.real + I * pycx.imag; + if (++j >= size) + j = 0; + Py_XDECREF(obj); + } + if (inverse) { // Normalize using 1/N + fftw_execute(planB); // Calculate inverse FFT / N + if (window) { + for (i = 0; i < fft_size; i++) // multiply by window / N + samples[i] *= fft_window[i] / size; + } + else { + for (i = 0; i < fft_size; i++) // divide by N + samples[i] /= size; + } + } + else { + if (window) { + for (i = 0; i < fft_size; i++) // multiply by window + samples[i] *= fft_window[i]; + } + fftw_execute(planF); // Calculate FFT + } + pyseq = PyList_New(fft_size); + j = (size - 1) / 2; // zero frequency in input + for (i = 0; i < fft_size; i++) { + pycx.real = creal(samples[i]); + pycx.imag = cimag(samples[i]); + PyList_SetItem(pyseq, j, PyComplex_FromCComplex(pycx)); + if (++j >= size) + j = 0; + } + return pyseq; +} + +static PyObject * dft(PyObject * self, PyObject * args) +{ + PyObject * tuple2; + int window; + + window = 0; + if (!PyArg_ParseTuple (args, "O|i", &tuple2, &window)) + return NULL; + return Xdft(tuple2, 0, window); +} + +static PyObject * is_cwkey_down(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + if (QUISK_CWKEY_DOWN) + return PyInt_FromLong(1); + else + return PyInt_FromLong(0); +} + +static PyObject * is_key_down(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + if (quisk_is_key_down()) + return PyInt_FromLong(1); + else + return PyInt_FromLong(0); +} + +static PyObject * idft(PyObject * self, PyObject * args) +{ + PyObject * tuple2; + int window; + + window = 0; + if (!PyArg_ParseTuple (args, "O|i", &tuple2, &window)) + return NULL; + return Xdft(tuple2, 1, window); +} + +void quisk_set_key_down(int state) +{ // Set the key state internally. Treat as a software PTT except that it works in CW mode too. + if (state) + key_is_down = 1; + else + key_is_down = 0; + quisk_set_play_state(); +} + +int quisk_is_key_down(void) +{ + return quisk_transmit_mode || quisk_play_state > RECEIVE; +} + +static void set_stone(void) +{ + if ( ! quisk_use_sidetone || quisk_isFDX) + quisk_active_sidetone = 0; // No sidetone + else if (quisk_Playback.driver == DEV_DRIVER_ALSA) + quisk_active_sidetone = 3; // Sidetone from alsa + else if (quisk_Playback.driver == DEV_DRIVER_PULSEAUDIO) + quisk_active_sidetone = 4; // Sidetone from pulseaudio + else if (quisk_Playback.driver == DEV_DRIVER_WASAPI2) + quisk_active_sidetone = 1; // Sidetone from wasapi + else + quisk_active_sidetone = 2; // Sidetone from old logic +} + +#define IS_HW_CWKEY quisk_hardware_cwkey +#define IS_SW_CWKEY (quisk_serial_key_down || quisk_midi_cwkey || quisk_remote_cwkey) +#define IS_HW_PTT hardware_ptt +#define IS_SW_PTT (quisk_serial_ptt || key_is_down || is_PTT_down) + +void quisk_set_play_state(void) +{ + static double Time0; // Timer to change to RX after the key goes up. CW hang time and PTT. + static double TimeoutTimer = 1E30; // Timer for maximum Tx time limit. + //static int change = 0; + //int i; + + if (quisk_use_rx_udp == 10 && IS_HW_CWKEY && quisk_play_state != HARDWARE_CWKEY) { + // work around HL2 gateware bug that uses CWX when key goes down + Time0 = TimeoutTimer = QuiskTimeSec(); + quisk_play_state = HARDWARE_CWKEY; + set_stone(); + quisk_play_sidetone(&quisk_Playback); + hermes_mox_bit = 0; + } + switch (quisk_play_state) { + case SHUTDOWN: + Time0 = QuiskTimeSec(); + quisk_active_sidetone = 0; + hermes_mox_bit = 0; + break; + case STARTING: + quisk_active_sidetone = 0; + if (QuiskTimeSec() - Time0 > 0.500) { + quisk_play_state = RECEIVE; + if (quisk_sound_state.verbose_sound) + QuiskPrintf("Change from state Starting to Receive\n"); + } + hermes_mox_bit = 0; + break; + case RECEIVE: + quisk_active_sidetone = 0; + if (rxMode == CWU || rxMode == CWL) { + if (key_is_down) { + Time0 = TimeoutTimer = QuiskTimeSec(); + quisk_play_state = SOFTWARE_PTT; + hermes_mox_bit = 1; + } + else if (IS_HW_CWKEY) { + Time0 = TimeoutTimer = QuiskTimeSec(); + quisk_play_state = HARDWARE_CWKEY; + set_stone(); + quisk_play_sidetone(&quisk_Playback); + hermes_mox_bit = 1; + } + else if (IS_SW_CWKEY) { + Time0 = TimeoutTimer = QuiskTimeSec(); + quisk_play_state = SOFTWARE_CWKEY; + set_stone(); + quisk_play_sidetone(&quisk_Playback); + hermes_mox_bit = 1; + } + } + else { + if (IS_HW_PTT) { + Time0 = TimeoutTimer = QuiskTimeSec(); + quisk_play_state = HARDWARE_PTT; + hermes_mox_bit = 1; + } + else if (IS_SW_PTT) { + Time0 = TimeoutTimer = QuiskTimeSec(); + quisk_play_state = SOFTWARE_PTT; + hermes_mox_bit = 1; + } + } + break; + case HARDWARE_CWKEY: + if (IS_HW_CWKEY) + Time0 = QuiskTimeSec(); + else if (QuiskTimeSec() - Time0 >= quisk_sound_state.quiskKeyupDelay * 1E-3) { + quisk_play_state = RECEIVE; + quisk_play_sidetone(&quisk_Playback); + quisk_active_sidetone = 0; + hermes_mox_bit = 0; + } + break; + case SOFTWARE_CWKEY: + if (IS_SW_CWKEY) + Time0 = QuiskTimeSec(); + else if (QuiskTimeSec() - Time0 >= quisk_sound_state.quiskKeyupDelay * 1E-3) { + quisk_play_state = RECEIVE; + quisk_play_sidetone(&quisk_Playback); + quisk_active_sidetone = 0; + hermes_mox_bit = 0; + } + break; + case HARDWARE_PTT: + quisk_active_sidetone = 0; + if (IS_HW_PTT) + Time0 = QuiskTimeSec(); + else if (QuiskTimeSec() - Time0 >= 50E-3) { + quisk_play_state = RECEIVE; + hermes_mox_bit = 0; + } + break; + case SOFTWARE_PTT: + quisk_active_sidetone = 0; + if (IS_SW_PTT) + Time0 = QuiskTimeSec(); + else if (QuiskTimeSec() - Time0 >= 50E-3) { + quisk_play_state = RECEIVE; + hermes_mox_bit = 0; + } + break; + } + if (maximum_tx_secs && quisk_play_state > RECEIVE && QuiskTimeSec() - TimeoutTimer >= maximum_tx_secs) { + quisk_hardware_cwkey = 0; + quisk_serial_key_down = 0; + quisk_serial_ptt = 0; + quisk_midi_cwkey = 0; + hardware_ptt = 0; + key_is_down = 0; + is_PTT_down = 0; + hermes_mox_bit = 0; + } + //i = quisk_play_state + hermes_mox_bit * 19 + IS_HW_PTT * 100 + IS_HW_CWKEY * 1000 + IS_SW_PTT * 10000 + IS_SW_CWKEY * 100000; + //if (i != change) { + // change = i; + // QuiskPrintf("quisk_play_state %d hermes_mox_bit %d IS_HW_PTT %d IS_HW_CWKEY %d IS_SW_PTT %d IS_SW_CWKEY %d\n", + // quisk_play_state, hermes_mox_bit, IS_HW_PTT, IS_HW_CWKEY, IS_SW_PTT, IS_SW_CWKEY); + //} +} + +static PyObject * record_app(PyObject * self, PyObject * args) +{ // Record the Python object for the application instance, malloc space for fft's. + int i, j, rate; + unsigned long handle; + fftw_complex * pt; + char * name; + const char * utf8 = "utf-8"; + Py_ssize_t l1; + + name = malloc(QUISK_SC_SIZE); + l1 = QUISK_SC_SIZE; + if (!PyArg_ParseTuple (args, "OOiiiiikes#", &pyApp, &quisk_pyConfig, &data_width, &graph_width, + &fft_size, &multirx_data_width, &rate, &handle, utf8, &name, &l1)) + return NULL; + strMcpy(fftw_wisdom_name, name, QUISK_SC_SIZE); + free(name); + + Py_INCREF(quisk_pyConfig); + +#ifdef MS_WINDOWS +#ifdef _WIN64 + quisk_mainwin_handle = (HWND)(unsigned long long)handle; +#else + quisk_mainwin_handle = (HWND)handle; +#endif +#endif + fftw_import_wisdom_from_filename(fftw_wisdom_name); + rx_udp_clock = QuiskGetConfigDouble("rx_udp_clock", 122.88e6); + graph_refresh = QuiskGetConfigInt("graph_refresh", 7); + quisk_use_rx_udp = QuiskGetConfigInt("use_rx_udp", 0); + quisk_sidetoneFreq = QuiskGetConfigInt("cwTone", 700); + waterfall_scroll_mode = QuiskGetConfigInt("waterfall_scroll_mode", 1); + quisk_use_sidetone = QuiskGetConfigInt("use_sidetone", 0); + quisk_start_cw_delay = QuiskGetConfigInt("start_cw_delay", 15); + quisk_start_ssb_delay = QuiskGetConfigInt("start_ssb_delay", 100); + maximum_tx_secs = QuiskGetConfigInt("maximum_tx_secs", 0); + quisk_sound_state.sample_rate = rate; + fft_sample_rate = rate; + is_little_endian = 1; // Test machine byte order + if (*(char *)&is_little_endian == 1) + is_little_endian = 1; + else + is_little_endian = 0; + strMcpy (quisk_sound_state.err_msg, CLOSED_TEXT, QUISK_SC_SIZE); + // Initialize space for the FFTs + for (i = 0; i < FFT_ARRAY_SIZE; i++) { + fft_data_array[i].filled = 0; + fft_data_array[i].index = 0; + fft_data_array[i].block = 0; + fft_data_array[i].samples = (fftw_complex *) fftw_malloc(sizeof(fftw_complex) * fft_size); + } + pt = fft_data_array[0].samples; + quisk_fft_plan = quisk_create_or_cache_fftw_plan_dft_1d(fft_size, pt, pt, FFTW_FORWARD, FFTW_MEASURE); + // Create space for the fft average and window + if (fft_window) + free(fft_window); + fft_window = (double *) malloc(sizeof(double) * fft_size); + for (i = 0, j = -fft_size / 2; i < fft_size; i++, j++) { + if (0) // Hamming + fft_window[i] = 0.54 + 0.46 * cos(2. * M_PI * j / fft_size); + else // Hanning + fft_window[i] = 0.5 + 0.5 * cos(2. * M_PI * j / fft_size); + } + // Initialize plan for multirx FFT + multirx_fft_width = multirx_data_width * MULTIRX_FFT_MULT; // Use larger FFT than graph size + multirx_fft_next_samples = (fftw_complex *)malloc(multirx_fft_width * sizeof(fftw_complex)); + multirx_fft_next_plan = quisk_create_or_cache_fftw_plan_dft_1d(multirx_fft_width, multirx_fft_next_samples, multirx_fft_next_samples, FFTW_FORWARD, FFTW_MEASURE); + if (current_graph) + free(current_graph); + current_graph = (double *) malloc(sizeof(double) * data_width); + measure_freq(NULL, 0, 0); + dAutoNotch(NULL, 0, 0, 0); + quisk_process_decimate(NULL, 0, 0, 0); + quisk_process_demodulate(NULL, NULL, 0, 0, 0, 0); +#if DEBUG_IO + QuiskPrintTime(NULL, 0); +#endif + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * record_graph(PyObject * self, PyObject * args) +{ /* record the Python object for the application instance */ + if (!PyArg_ParseTuple (args, "iid", &graphX, &graphY, &graphScale)) + return NULL; + graphScale *= 2; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * test_1(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * test_2(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * test_3(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "")) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_fdx(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "i", &quisk_isFDX)) + return NULL; + quisk_set_play_state(); + Py_INCREF (Py_None); + return Py_None; +} + +static PyObject * set_sample_bytes(PyObject * self, PyObject * args) +{ + if (!PyArg_ParseTuple (args, "i", &sample_bytes)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +static PyMethodDef QuiskMethods[] = { + {"add_tone", add_tone, METH_VARARGS, "Add a test tone to the data."}, + {"dft", dft, METH_VARARGS, "Calculate the discrete Fourier transform."}, + {"idft", idft, METH_VARARGS, "Calculate the inverse discrete Fourier transform."}, + {"is_key_down", is_key_down, METH_VARARGS, "Check whether the key is down; return 0 or 1."}, + {"is_cwkey_down", is_cwkey_down, METH_VARARGS, "Check whether the CW key is down; return 0 or 1."}, + {"get_state", get_state, METH_VARARGS, "Return a count of read and write errors."}, + {"get_graph", get_graph, METH_VARARGS, "Return a tuple of graph data."}, + {"get_bandscope", get_bandscope, METH_VARARGS, "Return a tuple of bandscope data."}, + {"set_multirx_mode", set_multirx_mode, METH_VARARGS, "Select demodulation mode for sub-receivers."}, + {"set_multirx_freq", set_multirx_freq, METH_VARARGS, "Select how to play audio from sub-receivers."}, + {"set_multirx_play_method", set_multirx_play_method, METH_VARARGS, "Select how to play audio from sub-receivers."}, + {"set_multirx_play_channel", set_multirx_play_channel, METH_VARARGS, "Select which sub-receiver to play audio."}, + {"get_multirx_graph", get_multirx_graph, METH_VARARGS, "Return a tuple of sub-receiver graph data."}, + {"get_filter", get_filter, METH_VARARGS, "Return the frequency response of the receive filter."}, + {"get_filter_rate", get_filter_rate, METH_VARARGS, "Return the sample rate used for the filters."}, + {"get_tx_filter", quisk_get_tx_filter, METH_VARARGS, "Return the frequency response of the transmit filter."}, + {"get_audio_graph", get_audio_graph, METH_VARARGS, "Return a tuple of the audio graph data."}, + {"measure_frequency", measure_frequency, METH_VARARGS, "Set the method, return the measured frequency."}, + {"measure_audio", measure_audio, METH_VARARGS, "Set the method, return the measured audio voltage."}, + {"get_overrange", get_overrange, METH_VARARGS, "Return the count of overrange (clip) for the ADC."}, + {"get_smeter", get_smeter, METH_VARARGS, "Return the S meter reading."}, + {"get_hermes_adc", get_hermes_adc, METH_VARARGS, "Return the ADC peak level."}, + {"get_hermes_TFRC", get_hermes_TFRC, METH_VARARGS, "Return the temperature, forward and reverse power and PA current."}, + {"set_hermes_id", set_hermes_id, METH_VARARGS, "Set the Hermes hardware code version and board ID."}, + {"set_hermes_filters", quisk_set_hermes_filter, METH_VARARGS, "Set the Hermes filter to use for Rx and Tx."}, + {"invert_spectrum", invert_spectrum, METH_VARARGS, "Invert the input RF spectrum"}, + {"ip_interfaces", ip_interfaces, METH_VARARGS, "Return a list of interface data"}, + {"pc_to_hermes", pc_to_hermes, METH_VARARGS, "Send this block of control data to the Hermes device"}, + {"pc_to_hermeslite_writequeue", pc_to_hermeslite_writequeue, METH_VARARGS, "Fill Hermes-Lite write queue"}, + {"set_hermeslite_writepointer", set_hermeslite_writepointer, METH_VARARGS, "Set Hermes-Lite write pointer"}, + {"get_hermeslite_writepointer", get_hermeslite_writepointer, METH_VARARGS, "Return Hermes-Lite write pointer"}, + {"clear_hermeslite_response", clear_hermeslite_response, METH_VARARGS, "Clear the Hermes-Lite response array"}, + {"get_hermeslite_response", get_hermeslite_response, METH_VARARGS, "Get the Hermes-Lite response array"}, + {"hermes_to_pc", hermes_to_pc, METH_VARARGS, "Get the block of control data from the Hermes device"}, + {"record_app", record_app, METH_VARARGS, "Save the App instance."}, + {"record_graph", record_graph, METH_VARARGS, "Record graph parameters."}, + {"ImmediateChange", ImmediateChange, METH_VARARGS, "Call this to notify the program of changes."}, + {"set_ampl_phase", quisk_set_ampl_phase, METH_VARARGS, "Set the sound card amplitude and phase corrections."}, + {"set_udp_tx_correct", quisk_set_udp_tx_correct, METH_VARARGS, "Set the UDP transmit corrections."}, + {"set_agc", set_agc, METH_VARARGS, "Set the AGC parameters."}, + {"set_squelch", set_squelch, METH_VARARGS, "Set the FM squelch parameter."}, + {"get_squelch", get_squelch, METH_VARARGS, "Get the FM squelch state, 0 or 1."}, + {"set_ssb_squelch", set_ssb_squelch, METH_VARARGS, "Set the SSB squelch parameters."}, + {"set_ctcss", set_ctcss, METH_VARARGS, "Set the frequency of the repeater access tone."}, + {"set_file_name", (PyCFunction)quisk_set_file_name, METH_VARARGS|METH_KEYWORDS, "Set the names and state of the recording and playback files."}, + {"get_params", get_params, METH_VARARGS, "Return parameters from quisk."}, + {"set_params", (PyCFunction)set_params, METH_VARARGS|METH_KEYWORDS, "Set miscellaneous parameters in quisk.c."}, + {"set_sparams", (PyCFunction)quisk_set_sparams, METH_VARARGS|METH_KEYWORDS, "Set miscellaneous parameters in sound.c."}, + {"set_filters", set_filters, METH_VARARGS, "Set the receive audio I and Q channel filters."}, + {"set_auto_notch", set_auto_notch, METH_VARARGS, "Set the auto notch on or off."}, + {"set_kill_audio", set_kill_audio, METH_VARARGS, "Replace radio sound with silence."}, + {"set_enable_bandscope", set_enable_bandscope, METH_VARARGS, "Enable or disable sending bandscope data."}, + {"set_noise_blanker", set_noise_blanker, METH_VARARGS, "Set the noise blanker level."}, + {"set_record_state", set_record_state, METH_VARARGS, "Set the temp buffer record and playback state."}, + {"set_rx_mode", set_rx_mode, METH_VARARGS, "Set the receive mode: CWL, USB, AM, etc."}, + {"set_mic_out_volume", set_mic_out_volume, METH_VARARGS, "Set the level of the mic output for SoftRock transmit"}, + {"set_spot_level", set_spot_level, METH_VARARGS, "Set the spot level, or -1 for no spot"}, + {"set_imd_level", set_imd_level, METH_VARARGS, "Set the imd level 0 to 1000."}, + {"set_sidetone", set_sidetone, METH_VARARGS, "Set the sidetone volume and frequency."}, + {"set_sample_bytes", set_sample_bytes, METH_VARARGS, "Set the number of bytes for each I or Q sample."}, + {"XXset_transmit_mode", set_transmit_mode, METH_VARARGS, "Change the radio to transmit mode independent of key_down."}, + {"set_volume", set_volume, METH_VARARGS, "Set the audio output volume."}, + {"set_tx_audio", (PyCFunction)quisk_set_tx_audio, METH_VARARGS|METH_KEYWORDS, "Set the transmit audio parameters."}, + {"is_vox", quisk_is_vox, METH_VARARGS, "return the VOX state zero or one."}, + {"set_split_rxtx", set_split_rxtx, METH_VARARGS, "Set split for rx/tx."}, + {"set_tune", set_tune, METH_VARARGS, "Set the tuning frequency."}, + {"test_1", test_1, METH_VARARGS, "Test 1 function."}, + {"test_2", test_2, METH_VARARGS, "Test 2 function."}, + {"test_3", test_3, METH_VARARGS, "Test 3 function."}, + {"tx_hold_state", tx_hold_state, METH_VARARGS, "Query or set the transmit hold state."}, + {"set_fdx", set_fdx, METH_VARARGS, "Set full duplex mode; ignore the key status."}, + {"directx_sound_devices", quisk_directx_sound_devices, METH_VARARGS, "Return a list of available DirectX sound device names."}, + {"wasapi_sound_devices", quisk_wasapi_sound_devices, METH_VARARGS, "Return a list of available WASAPI sound device names."}, + {"portaudio_sound_devices", quisk_portaudio_sound_devices, METH_VARARGS, "Return a list of available PortAudio sound device names."}, + {"pulseaudio_sound_devices", quisk_pulseaudio_sound_devices, METH_VARARGS, "Return a list of available PulseAudio sound device names."}, + {"alsa_sound_devices", quisk_alsa_sound_devices, METH_VARARGS, "Return a list of available Alsa sound device names."}, + {"GetQuiskPrintf", GetQuiskPrintf, METH_VARARGS, "Return the output of our printf() replacement from Windows C."}, + {"AppStatus", AppStatus, METH_VARARGS, "Perform application initialization."}, + {"sound_errors", quisk_sound_errors, METH_VARARGS, "Return a list of text strings with sound devices and error counts"}, + {"set_sound_name", quisk_set_sound_name, METH_VARARGS, "Set the name of the soundcard device."}, + {"open_sound", open_sound, METH_VARARGS, "Open the soundcard device."}, + {"control_midi", (PyCFunction)quisk_control_midi, METH_VARARGS|METH_KEYWORDS, "Set the MIDI parameters."}, + {"open_wav_file_play", open_wav_file_play, METH_VARARGS, "Open a WAV file to play instead of the microphone."}, + {"close_sound", close_sound, METH_VARARGS, "Stop the soundcard and release resources."}, + {"capt_channels", quisk_capt_channels, METH_VARARGS, "Set the I and Q capture channel numbers"}, + {"play_channels", quisk_play_channels, METH_VARARGS, "Set the I and Q playback channel numbers"}, + {"micplay_channels", quisk_micplay_channels, METH_VARARGS, "Set the I and Q microphone playback channel numbers"}, + {"change_scan", change_scan, METH_VARARGS, "Change to a new FFT rate and multiplier"}, + {"change_rate", change_rate, METH_VARARGS, "Change to a new sample rate"}, + {"change_rates", change_rates, METH_VARARGS, "Change to multiple new sample rates"}, + {"read_sound", read_sound, METH_VARARGS, "Read from the soundcard."}, + {"start_sound", start_sound, METH_VARARGS, "Start the soundcard."}, + {"mixer_set", mixer_set, METH_VARARGS, "Set microphone mixer parameters such as volume."}, + {"open_key", (PyCFunction)quisk_open_key, METH_VARARGS|METH_KEYWORDS, "Open access to the state of the key (CW or PTT)."}, + {"close_key", quisk_close_key, METH_VARARGS, "Close the key."}, + {"open_rx_udp", open_rx_udp, METH_VARARGS, "Open a UDP port for capture."}, + {"close_rx_udp", close_rx_udp, METH_VARARGS, "Close the UDP port used for capture."}, + {"add_rx_samples", add_rx_samples, METH_VARARGS, "Record the Rx samples received by Python code."}, + {"add_bscope_samples", add_bscope_samples, METH_VARARGS, "Record the bandscope samples received by Python code."}, + {"set_key_down", set_key_down, METH_VARARGS, "Change the key up/down state."}, + {"set_cwkey", set_hardware_cwkey, METH_VARARGS, "Change the CW key up/down state."}, + {"set_remote_cwkey", set_remote_cwkey, METH_VARARGS, "Change the remote control CW key up/down state."}, + {"set_PTT", set_PTT, METH_VARARGS, "Record the PTT button state."}, + {"freedv_open", quisk_freedv_open, METH_VARARGS, "Open FreeDV."}, + {"freedv_close", quisk_freedv_close, METH_VARARGS, "Close FreeDV."}, + {"freedv_get_snr", quisk_freedv_get_snr, METH_VARARGS, "Return the signal to noise ratio in dB."}, + {"freedv_get_version", quisk_freedv_get_version, METH_VARARGS, "Return the codec2 API version."}, + {"freedv_get_rx_char", quisk_freedv_get_rx_char, METH_VARARGS, "Get text characters received from freedv."}, + {"freedv_set_options", (PyCFunction)quisk_freedv_set_options, METH_VARARGS|METH_KEYWORDS, "Set the freedv parameters."}, + {"freedv_set_squelch_en", quisk_freedv_set_squelch_en, METH_VARARGS, "Enable or disable FreeDV squelch."}, + {"wdsp_set_parameter", (PyCFunction)quisk_wdsp_set_parameter, METH_VARARGS|METH_KEYWORDS, "Set parameters for the WDSP SDR library."}, + {"tmp_record_save", tmp_record_save, METH_VARARGS, "Save the temporary recording in a WAV file."}, + {"watfall_RgbData", watfall_RgbData, METH_VARARGS, "Return a cookie for the Waterfall pixel data."}, + {"watfall_OnGraphData", watfall_OnGraphData, METH_VARARGS, "Record a row of Waterfall FFT dB data."}, + {"watfall_GetPixels", watfall_GetPixels, METH_VARARGS, "Write the Waterfall image to be displayed."}, + {"write_fftw_wisdom", write_fftw_wisdom, METH_VARARGS, "Write the current fftw wisdom to the wisdom file."}, + {"read_fftw_wisdom", read_fftw_wisdom, METH_VARARGS, "Return the current fftw wisdom as a byte array."}, +// Remote Quisk control head and slave by Ben, AC2YD + {"start_control_head_remote_sound", quisk_start_control_head_remote_sound, METH_VARARGS, "Start running UDP remote sound on control_head."}, + {"stop_control_head_remote_sound", quisk_stop_control_head_remote_sound, METH_VARARGS, "Stop running UDP remote sound on control_head."}, + {"start_remote_radio_remote_sound", quisk_start_remote_radio_remote_sound, METH_VARARGS, "Start running UDP remote sound on remote_radio."}, + {"stop_remote_radio_remote_sound", quisk_stop_remote_radio_remote_sound, METH_VARARGS, "Stop running UDP remote sound on remote_radio."}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +#if PY_MAJOR_VERSION < 3 +// Python 2.7: +PyMODINIT_FUNC init_quisk (void) +{ + PyObject * m; + PyObject * c_api_object; + static void * Quisk_API[] = QUISK_API_INIT; + + m = Py_InitModule ("_quisk", QuiskMethods); + if (m == NULL) { + QuiskPrintf("Py_InitModule of _quisk failed!\n"); + return; + } + + QuiskError = PyErr_NewException ("quisk.error", NULL, NULL); + Py_INCREF (QuiskError); + PyModule_AddObject (m, "error", QuiskError); + + /* Create Capsules for handing _quisk symbols to C extensions in other Python modules. */ + c_api_object = PyCapsule_New(Quisk_API, "_quisk.QUISK_C_API", NULL); + if (c_api_object != NULL) + PyModule_AddObject(m, "QUISK_C_API", c_api_object); +} + +// Python 3: +#else +static struct PyModuleDef _quiskmodule = { + PyModuleDef_HEAD_INIT, + "_quisk", + NULL, + -1, + QuiskMethods +} ; + +PyMODINIT_FUNC PyInit__quisk(void) +{ + PyObject * m; + PyObject * c_api_object; + static void * Quisk_API[] = QUISK_API_INIT; + + m = PyModule_Create(&_quiskmodule); + if (m == NULL) + return NULL; + QuiskError = PyErr_NewException("_quisk.error", NULL, NULL); + if (QuiskError == NULL) { + Py_DECREF(m); + return NULL; + } + Py_INCREF (QuiskError); + PyModule_AddObject (m, "error", QuiskError); + + /* Create Capsules for handing _quisk symbols to C extensions in other Python modules. */ + c_api_object = PyCapsule_New(Quisk_API, "_quisk.QUISK_C_API", NULL); + if (c_api_object != NULL) + PyModule_AddObject(m, "QUISK_C_API", c_api_object); + return m; +} +#endif diff --git a/quisk.h b/quisk.h new file mode 100644 index 0000000..f428e6c --- /dev/null +++ b/quisk.h @@ -0,0 +1,494 @@ + +#define DEBUG_IO 0 +#define DEBUG_MIC 0 + +// Sound parameters +// +#define QUISK_SC_SIZE 256 +#define QUISK_PATH_SIZE 256 // max file path length +#define IP_SIZE 32 +#define MAX_FILTER_SIZE 10001 +#define BIG_VOLUME 2.2e9 +#define CLOSED_TEXT "The sound device is closed." +#define CLIP32 2147483647 +#define CLIP16 32767 +#define SAMP_BUFFER_SIZE 66000 // size of arrays used to capture samples +#define IMD_TONE_1 1200 // frequency of IMD test tones +#define IMD_TONE_2 1600 +#define INTERP_FILTER_TAPS 85 // interpolation filter +#define MIC_OUT_RATE 48000 // mic post-processing sample rate +#define PA_LIST_SIZE 16 // max number of pulseaudio devices +#define QUISK_MAX_SUB_RECEIVERS 9 // Maximum number of sub-receiver channels in addition to the main receiver +#define QUISK_INDEX_SUB_RX1 4 // Index of sub-receiver Rx1 in quiskPlaybackDevices +#define START_CW_DELAY_MAX 250 // Maximum delay for start_cw_delay + +// Test the audio: 0 == No test; normal operation; +// 1 == Copy real data to the output; 2 == copy imaginary data to the output; +// 3 == Copy transmit audio to the output. +#define TEST_AUDIO 0 + +#define QUISK_CWKEY_DOWN (quisk_hardware_cwkey || quisk_serial_key_down || quisk_midi_cwkey || quisk_remote_cwkey) + +#ifdef MS_WINDOWS +#define QUISK_SHUT_RD SD_RECEIVE +#define QUISK_SHUT_BOTH SD_BOTH +#include +extern CRITICAL_SECTION QuiskCriticalSection; +extern PyObject * QuiskPrintf(char *, ...); +#else +#define SOCKET int +#define INVALID_SOCKET -1 +#define QUISK_SHUT_RD SHUT_RD +#define QUISK_SHUT_BOTH SHUT_RDWR +#define QuiskPrintf printf +#endif + +#if PY_MAJOR_VERSION >= 3 +#define PyInt_FromLong PyLong_FromLong +#define PyInt_Check PyLong_Check +#define PyInt_AsLong PyLong_AsLong +#define PyInt_AsUnsignedLongMask PyLong_AsUnsignedLongMask +#endif + +#define PyString_FromString PyUnicode_FromString + +typedef enum _rx_mode { + CWL = 0, + CWU = 1, + LSB = 2, + USB = 3, + AM = 4, + FM = 5, + EXT = 6, + DGT_U = 7, + DGT_L = 8, + DGT_IQ = 9, + IMD = 10, + FDV_U = 11, + FDV_L = 12, + DGT_FM = 13 +} rx_mode_type; + +// Pulseaudio support added by Philip G. Lee. Many thanks! +/*! + * \brief Specifies which driver a \c sound_dev is opened with + */ + +typedef enum { // In order of preference. + Int32, + Int16, + Float32, + Int24 +} sound_format_t; +// Keep this array of names up to date. +extern char * sound_format_names[4]; // = {"Int32", "Int16", "Float32", "Int24"} ; + +typedef enum { + t_Capture, + t_Playback, + t_MicCapture, + t_MicPlayback, + t_DigitalInput, + t_DigitalOutput, + t_RawSamplePlayback, + t_DigitalRx1Output +} device_index_t; + +typedef enum dev_driver{ + DEV_DRIVER_NONE = 0, + DEV_DRIVER_PORTAUDIO, + DEV_DRIVER_ALSA, + DEV_DRIVER_PULSEAUDIO, + DEV_DRIVER_DIRECTX, + DEV_DRIVER_WASAPI, + DEV_DRIVER_WASAPI2, +} dev_driver_t; + +typedef enum { + SHUTDOWN, + STARTING, + RECEIVE, +// All states greater than RECEIVE are assumed to be transmit states. + HARDWARE_CWKEY, + HARDWARE_PTT, + SOFTWARE_CWKEY, + SOFTWARE_PTT, + } play_state_t; + +struct sound_dev { // data for sound capture or playback device + char name[QUISK_SC_SIZE]; // the name of the device for display + char stream_description[QUISK_SC_SIZE]; // Short description of device/stream + char device_name[QUISK_SC_SIZE]; // hardware device name + void * handle; // Handle of open device, or NULL + dev_driver_t driver; // Which audio driver the device is using + void * buffer; // Handle of buffer for device + int portaudio_index; // index of portaudio device, or -1 + int doAmplPhase; // Amplitude and Phase corrections + double AmPhAAAA; + double AmPhCCCC; + double AmPhDDDD; + double portaudio_latency; // Suggested latency for portaudio device + int sample_rate; // Sample rate such as 48000, 96000, 192000 + int sample_bytes; // Size of one channel sample in bytes, either 2 or 3 or 4 + int num_channels; // number of channels per frame: 1, 2, 3, ... + int channel_I; // Index of I and Q channels: 0, 1, ... + int channel_Q; + int channel_Delay; // Delay this channel by one sample; -1 for no delay, else channel_I or _Q + int overrange; // Count for ADC overrange (clip) for device + // Number of frames for a read request. + // If 0, the read should be non-blocking and read all available + // frames. + int read_frames; + int latency_frames; // desired latency in audio play samples + int play_buf_size; // size of the sound card playback buffer in frames (?? or bytes) + int play_buf_bytes; // size of the sound card playback buffer in bytes + int old_key; // previous key up/down state + int use_float; // DirectX: Use IEEE floating point + int dataPos; // DirectX: data position + int play_delay; // DirectX: bytes of sound available to play + int old_play_delay; // DirectX: previous bytes of sound available to play + int started; // DirectX: started flag or state + int dev_error; // read or write error + int dev_underrun; // lack of samples to play + int dev_latency; // latency frames + unsigned int rate_min; // min and max available sample rates + unsigned int rate_max; + unsigned int chan_min; // min and max available number of channels + unsigned int chan_max; + complex double dc_remove; // filter to remove DC from samples + double save_sample; // Used to delay the I or Q sample + char msg1[QUISK_SC_SIZE]; // string for information message + char dev_errmsg[QUISK_SC_SIZE]; // error message for device, or "" + int stream_dir_record; // 1 for recording, 0 for playback + char server[IP_SIZE]; // server string for remote pulseaudio + int stream_format; // format of pulseaudio device + int pulse_stream_state; // state of the pulseaudio stream + volatile int cork_status; // 1 for corked, 0 for uncorked + double average_square; // average of squared sample magnitude + sound_format_t sound_format; // format of sound array for the sound device + device_index_t dev_index; // identify devices + void * device_data; // special data for each sound device + double TimerTime0; // Used to print debug messages + // Variables used to correct differences in sample rates: + int cr_correction; + int cr_delay; + double cr_average_fill; + int cr_average_count; + int cr_sample_time; + int cr_correct_time; +} ; + +extern struct sound_dev quisk_Playback; +extern struct sound_dev quisk_MicPlayback; + +struct sound_conf { + char dev_capt_name[QUISK_SC_SIZE]; + char dev_play_name[QUISK_SC_SIZE]; + int sample_rate; // Input sample rate from the ADC + int playback_rate; // Output play rate to sound card + int data_poll_usec; + int latency_millisecs; + unsigned int rate_min; + unsigned int rate_max; + unsigned int chan_min; + unsigned int chan_max; + int read_error; + int write_error; + int underrun_error; + int overrange; // count of ADC overrange (clip) for non-soundcard device + int latencyCapt; + int latencyPlay; + int interrupts; + char msg1[QUISK_SC_SIZE]; + char err_msg[QUISK_SC_SIZE]; + // These parameters are for the microphone: + char mic_dev_name[QUISK_SC_SIZE]; // capture device + char name_of_mic_play[QUISK_SC_SIZE]; // playback device + char mic_ip[IP_SIZE]; + int mic_sample_rate; // capture sample rate + int mic_playback_rate; // playback sample rate + int tx_audio_port; + int mic_read_error; + int mic_channel_I; // channel number for microphone: 0, 1, ... + int mic_channel_Q; + double mic_out_volume; + char IQ_server[IP_SIZE]; //IP address of optional streaming IQ server (pulseaudio) + int verbose_pulse; // verbose output for pulse audio + int verbose_sound; // verbose output for other sound systems + int quiskKeyupDelay; // keup delay in milliseconds +} ; + +enum quisk_rec_state { + IDLE, + TMP_RECORD_SPEAKERS, + TMP_RECORD_MIC, + TMP_PLAY_SPKR_MIC, + FILE_PLAY_SPKR_MIC, + FILE_PLAY_SAMPLES } ; +extern enum quisk_rec_state quisk_record_state; + +struct wav_file { + FILE * fp; + char file_name[QUISK_PATH_SIZE]; + unsigned long samples; +}; + +struct QuiskWav { // data to create a WAV or RAW audio file + double scale; + int sample_rate; + short format; // RAW is 0; PCM integer is 1; IEEE float is 3. + short nChan; + short bytes_per_sample; + FILE * fp; + unsigned int samples; + int fpStart; + int fpEnd; + int fpPos; +} ; + +// Remote Quisk control head and slave by Ben, AC2YD +extern int remote_control_head; +extern int remote_control_slave; +int read_remote_radio_sound_socket(complex double * cSamples); +int read_remote_mic_sound_socket(complex double * cSamples); +void send_remote_radio_sound_socket(complex double * cSamples, int nSamples); +void send_remote_mic_sound_socket(complex double * cSamples, int nSamples); +extern PyObject * quisk_start_control_head_remote_sound(PyObject * self, PyObject * args); +extern PyObject * quisk_stop_control_head_remote_sound(PyObject * self, PyObject * args); +extern PyObject * quisk_start_remote_radio_remote_sound(PyObject * self, PyObject * args); +extern PyObject * quisk_stop_remote_radio_remote_sound(PyObject * self, PyObject * args); +extern int receive_graph_data(double * fft_avg); +extern void send_graph_data(double * fft_avg, int fft_size, double zoom, double deltaf, int fft_sample_rate, double scale); + +void QuiskWavClose(struct QuiskWav *); +int QuiskWavWriteOpen(struct QuiskWav *, char *, short, short, short, int, double); +void QuiskWavWriteC(struct QuiskWav *, complex double *, int); +void QuiskWavWriteD(struct QuiskWav *, double *, int); +void QuiskWavWriteF(struct QuiskWav *, float *, int); +int QuiskWavReadOpen(struct QuiskWav *, char *, short, short, short, int, double); +void QuiskWavReadC(struct QuiskWav *, complex double *, int); +void QuiskWavReadD(struct QuiskWav *, double *, int); +void QuiskMeasureRate(const char *, int, int, int); +void quisk_record_audio(struct wav_file *, complex double *, int); +void copy2pixels(double * pixels, int n_pixels, double * fft, int fft_size, double zoom, double deltaf, double rate); + +extern struct sound_conf quisk_sound_state, * pt_quisk_sound_state; +extern int mic_max_display; // display value of maximum microphone signal level +extern int quiskSpotLevel; // 0 for no spotting; else the level 10 to 1000 +extern int data_width; +extern int quisk_using_udp; // is a UDP port used for capture (0 or 1)? +extern int quisk_rx_udp_started; // have we received any data? +extern rx_mode_type rxMode; // mode CWL, USB, etc. +extern int quisk_tx_tune_freq; // Transmit tuning frequency as +/- sample_rate / 2 +extern PyObject * quisk_pyConfig; // Configuration module instance +extern double quisk_mic_preemphasis; // Mic preemphasis 0.0 to 1.0; or -1.0 +extern double quisk_mic_clip; // Mic clipping; try 3.0 or 4.0 +extern int quisk_noise_blanker; // Noise blanker level, 0 for off +extern int quisk_sidetoneCtrl; // sidetone control value 0 to 1000 +extern double quisk_audioVolume; // volume control for radio sound playback, 0.0 to 1.0 +extern int quiskImdLevel; // level for rxMode IMD +extern int quiskTxHoldState; // state machine for Tx wait for repeater frequency shift +extern double quisk_ctcss_freq; // frequency in Hertz +extern unsigned char quisk_pc_to_hermes[17 * 4]; // Data to send from the PC to the Hermes hardware +extern unsigned char quisk_hermeslite_writequeue[5]; // One-time writes to Hermes-Lite +extern unsigned int quisk_hermeslite_writepointer; // 0==No request; 1=Send writequeue; 2==Wait for ACK; 3==0x3F error from HL2 +extern unsigned int quisk_hermes_code_version; // Hermes code version from Hermes to PC +extern unsigned int quisk_hermes_board_id; // Hermes board ID from Hermes to PC +extern int hermes_mox_bit; // Hermes mox bit from the PC to Hermes +extern int quisk_use_rx_udp; // Method of access to UDP hardware +extern complex double cRxFilterOut(complex double, int, int); +extern int quisk_multirx_count; // number of additional receivers zero or 1, 2, 3, .. +extern struct sound_dev quisk_DigitalRx1Output; // Output sound device for sub-receiver 1 +extern int quisk_is_vna; // is this the VNA program? +extern int quisk_serial_key_errors; // Error count for the Quisk internal serial key +extern double quisk_sidetoneVolume; // Audio output level of the CW sidetone, 0.0 to 1.0 +extern int quisk_serial_key_down; // The state of the serial port CW key +extern int quisk_serial_ptt; // The state of the serial port PTT +extern int quisk_hardware_cwkey; // The state of the hardware CW key from UDP or USB +extern int quisk_midi_cwkey; // The state of the MIDI CW key +extern int quisk_remote_cwkey; // The state of the remote CW key +extern int quisk_sidetoneFreq; // Frequency in hertz for the sidetone +extern int quisk_active_sidetone; // Whether and how to generate a sidetone +extern int quisk_isFDX; // Are we in full duplex mode? +extern int quisk_use_serial_port; // Are we using the serial port for CW key or PTT? +extern play_state_t quisk_play_state; // startup, receiving, sidetone +extern int freedv_current_mode; // current FreeDV mode; 700D, @)@) etc. +extern int n_modem_sample_rate; // Receive data, decimate to modem_sample_rate, FreeDV codec output data at speech_sample_rate +extern int n_speech_sample_rate; // Microphone decimate to speech_sample_rate, Freedv codec output data at modem_sample_rate, interpolate to 48000 +extern int quisk_start_cw_delay; // milliseconds to delay output on serial or MIDI CW key down +extern int quisk_start_ssb_delay; // milliseconds to discard output for all modes except CW +extern struct sound_dev * quiskPlaybackDevices[]; // array of Playback sound devices +extern int quisk_close_file_play; + +extern PyObject * quisk_set_spot_level(PyObject * , PyObject *); +extern PyObject * quisk_get_tx_filter(PyObject * , PyObject *); + +extern PyObject * quisk_set_ampl_phase(PyObject * , PyObject *); +extern PyObject * quisk_capt_channels(PyObject * , PyObject *); +extern PyObject * quisk_play_channels(PyObject * , PyObject *); +extern PyObject * quisk_micplay_channels(PyObject * , PyObject *); +extern PyObject * quisk_alsa_sound_devices(PyObject * , PyObject *); +extern PyObject * quisk_directx_sound_devices(PyObject * , PyObject *); +extern PyObject * quisk_portaudio_sound_devices(PyObject * , PyObject *); +extern PyObject * quisk_pulseaudio_sound_devices(PyObject * , PyObject *); +extern PyObject * quisk_wasapi_sound_devices(PyObject * , PyObject *); +extern PyObject * quisk_dummy_sound_devices(PyObject * , PyObject *); +extern PyObject * quisk_sound_errors(PyObject *, PyObject *); +extern PyObject * quisk_set_file_record(PyObject *, PyObject *); +extern PyObject * quisk_set_file_name(PyObject *, PyObject *, PyObject *); +extern PyObject * quisk_set_tx_audio(PyObject *, PyObject *, PyObject *); +extern PyObject * quisk_is_vox(PyObject *, PyObject *); +extern PyObject * quisk_set_udp_tx_correct(PyObject *, PyObject *); +extern PyObject * quisk_set_hermes_filter(PyObject *, PyObject *); +extern PyObject * quisk_set_alex_hpf(PyObject *, PyObject *); +extern PyObject * quisk_set_alex_lpf(PyObject *, PyObject *); + +extern PyObject * quisk_freedv_open(PyObject *, PyObject *); +extern PyObject * quisk_freedv_close(PyObject *, PyObject *); +extern PyObject * quisk_freedv_get_snr(PyObject *, PyObject *); +extern PyObject * quisk_freedv_get_version(PyObject *, PyObject *); +extern PyObject * quisk_freedv_get_rx_char(PyObject *, PyObject *); +extern PyObject * quisk_freedv_set_options(PyObject *, PyObject *, PyObject *); +extern PyObject * quisk_set_sparams(PyObject *, PyObject *, PyObject *); +extern PyObject * quisk_freedv_set_squelch_en(PyObject *, PyObject *); +extern PyObject * quisk_open_key(PyObject *, PyObject *, PyObject *); +extern PyObject * quisk_close_key(PyObject *, PyObject *); +extern PyObject * quisk_set_sound_name(PyObject *, PyObject *); +extern PyObject * quisk_alsa_control_midi(PyObject *, PyObject *, PyObject *); +extern PyObject * quisk_wasapi_control_midi(PyObject *, PyObject *, PyObject *); + +// WDSP interface +#define QUISK_WDSP_RX 1 +extern PyObject * quisk_wdsp_set_parameter(PyObject *, PyObject *, PyObject *); +extern int wdspFexchange0(int, double *, int); + +// These function pointers are the Start/Stop/Read interface for +// the SDR-IQ and any other C-language extension modules that return +// radio data samples. +typedef void (* ty_sample_start)(void); +typedef void (* ty_sample_stop)(void); +typedef int (* ty_sample_read)(complex double *); +typedef int (* ty_sample_write)(complex double *, int); +extern ty_sample_write quisk_pt_sample_write; + +void quisk_open_sound(void); +void quisk_close_sound(void); +int quisk_process_samples(complex double *, int); +void quisk_play_samples(complex double *, int); +void quisk_play_zeros(int); +void quisk_start_sound(void); +int quisk_get_overrange(void); +void quisk_alsa_mixer_set(char *, int, PyObject *, char *, int); +int quisk_read_sound(void); +int quisk_process_microphone(int, complex double *, int); +void quisk_open_mic(void); +void quisk_close_mic(void); +void quisk_set_key_down(int); +void quisk_set_tx_mode(void); +void ptimer(int); +int quisk_extern_demod(complex double *, int, double); +void quisk_tmp_microphone(complex double *, int); +void quisk_tmp_record(complex double * , int, double); +void quisk_file_microphone(complex double *, int); +void quisk_file_playback(complex double *, int, double); +void quisk_tmp_playback(complex double *, int, double); +void quisk_hermes_tx_send(int, int *); +void quisk_udp_mic_error(char *); +void quisk_check_freedv_mode(void); +void quisk_calc_audio_graph(double, complex double *, double *, int, int); +double QuiskDeltaSec(int); +void * quisk_make_sidetone(struct sound_dev *, int); +void * quisk_make_txIQ(struct sound_dev *, int); +int quisk_play_sidetone(struct sound_dev *); +void quisk_set_play_state(void); +void quisk_poll_hardware_key(void); + +// Functions supporting digital voice codecs +typedef int (* ty_dvoice_codec_rx)(complex double *, double *, int, int); +typedef int (* ty_dvoice_codec_tx)(complex double *, double *, int); +extern ty_dvoice_codec_rx pt_quisk_freedv_rx; +extern ty_dvoice_codec_tx pt_quisk_freedv_tx; + +// Driver function definitions================================================= +int quisk_read_alsa(struct sound_dev *, complex double *); +void quisk_play_alsa(struct sound_dev *, int, complex double *, int, double); +void quisk_alsa_sidetone(struct sound_dev *); +void quisk_start_sound_alsa(struct sound_dev **, struct sound_dev **); +void quisk_close_sound_alsa(struct sound_dev **, struct sound_dev **); + +int quisk_read_portaudio(struct sound_dev *, complex double *); +void quisk_play_portaudio(struct sound_dev *, int, complex double *, int, double); +void quisk_pulseaudio_sidetone(struct sound_dev *); +void quisk_start_sound_portaudio(struct sound_dev **, struct sound_dev **); +void quisk_close_sound_portaudio(void); + +void play_sound_interface(struct sound_dev * , int, complex double * , int, double); + +int quisk_read_pulseaudio(struct sound_dev *, complex double *); +void quisk_play_pulseaudio(struct sound_dev *, int, complex double *, int, double); +void quisk_start_sound_pulseaudio(struct sound_dev **, struct sound_dev **); +void quisk_close_sound_pulseaudio(void); +void quisk_cork_pulseaudio(struct sound_dev *, int); +void quisk_flush_pulseaudio(struct sound_dev *); + +int quisk_read_directx(struct sound_dev *, complex double *); +void quisk_play_directx(struct sound_dev *, int, complex double *, int, double); +void quisk_start_sound_directx(struct sound_dev **, struct sound_dev **); +void quisk_close_sound_directx(struct sound_dev **, struct sound_dev **); + +int quisk_read_wasapi(struct sound_dev *, complex double *); +void quisk_write_wasapi(struct sound_dev *, int, complex double *, double); +void quisk_play_wasapi(struct sound_dev *, int, complex double *, double); +void quisk_start_sound_wasapi(struct sound_dev **, struct sound_dev **); +void quisk_close_sound_wasapi(struct sound_dev **, struct sound_dev **); +//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +/* +Functions defined below this point are available for export to other extension modules using the +standard Python CObject or Capsule interface. See the documentation in import_quisk_api.c. Note +that index zero is used for a structure pointer, not a function pointer. + +To add a function, declare it twice, use the next array index, and add it to QUISK_API_INIT. +Be very careful; here be dragons! +*/ + +#ifdef IMPORT_QUISK_API +// For use by modules that import the _quisk symbols +extern void ** Quisk_API; // array of pointers to functions and variables from module _quisk +int import_quisk_api(void); // used to initialize Quisk_API + +#define QuiskGetConfigInt (*( int (*) (const char *, int) )Quisk_API[1]) +#define QuiskGetConfigDouble (*( double (*) (const char *, double) )Quisk_API[2]) +#define QuiskGetConfigString (*( char * (*) (const char *, char *) )Quisk_API[3]) +#define QuiskTimeSec (*( double (*) (void) )Quisk_API[4]) +#define QuiskSleepMicrosec (*( void (*) (int) )Quisk_API[5]) +#define QuiskPrintTime (*( void (*) (const char *, int) )Quisk_API[6]) +#define quisk_sample_source (*( void (*) (ty_sample_start, ty_sample_stop, ty_sample_read) )Quisk_API[7]) +#define quisk_dvoice_freedv (*( void (*) (ty_dvoice_codec_rx, ty_dvoice_codec_tx) )Quisk_API[8]) +#define quisk_is_key_down (*( int (*) (void) )Quisk_API[9]) +#define quisk_sample_source4 (*( void (*) (ty_sample_start, ty_sample_stop, ty_sample_read, ty_sample_write) )Quisk_API[10]) +#define strMcpy (*( char * (*) (char *, const char *, size_t) )Quisk_API[11]) + +#else +// Used to export symbols from _quisk in quisk.c + +int QuiskGetConfigInt(const char *, int); +double QuiskGetConfigDouble(const char *, double); +char * QuiskGetConfigString(const char *, char *); +double QuiskTimeSec(void); +void QuiskSleepMicrosec(int); +void QuiskPrintTime(const char *, int); +void quisk_sample_source(ty_sample_start, ty_sample_stop, ty_sample_read); +void quisk_dvoice_freedv(ty_dvoice_codec_rx, ty_dvoice_codec_tx); +int quisk_is_key_down(void); +void quisk_sample_source4(ty_sample_start, ty_sample_stop, ty_sample_read, ty_sample_write); +char * strMcpy(char *, const char *, size_t); + +#define QUISK_API_INIT { \ + &quisk_sound_state, &QuiskGetConfigInt, &QuiskGetConfigDouble, &QuiskGetConfigString, &QuiskTimeSec, \ + &QuiskSleepMicrosec, &QuiskPrintTime, &quisk_sample_source, &quisk_dvoice_freedv, &quisk_is_key_down, \ + &quisk_sample_source4, &strMcpy \ + } + +#endif + diff --git a/quisk.py b/quisk.py new file mode 100644 index 0000000..662a208 --- /dev/null +++ b/quisk.py @@ -0,0 +1,6529 @@ +#!/usr/bin/env python3 + +# All QUISK software is Copyright (C) 2006-2021 by James C. Ahlstrom. +# This free software is licensed for use under the GNU General Public +# License (GPL), see http://www.opensource.org. +# Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!! + +"""The main program for Quisk, a software defined radio. + +Usage: python quisk.py [-c | --config config_file_path] +This can also be installed as a package and run as quisk.main(). +""" + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +# Change to the directory of quisk.py. This is necessary to import Quisk packages, +# to load other extension modules that link against _quisk.so, to find shared libraries *.dll and *.so, +# and to find ./__init__.py and ./help.html. +import sys, os +#print ('getcwd', os.getcwd()) +#print ('__file__', __file__) +os.chdir(os.path.normpath(os.path.dirname(__file__))) # change directory to the location of this script +if sys.path[0] != '': # Make sure the current working directory is on path + sys.path.insert(0, '') +#print ('getcwd', os.getcwd()) + +import wx, wx.html, wx.lib.stattext, wx.lib.colourdb, wx.grid +import math, cmath, time, traceback, string, select, subprocess +import threading, pickle, webbrowser, json +try: + from xmlrpc.client import ServerProxy +except ImportError: + from xmlrpclib import ServerProxy +import _quisk as QS +from quisk_widgets import * +from filters import Filters +import dxcluster +import configure +import quisk_conf_defaults as conf +import quisk_wdsp + +for name in configure.name2format: # Add defaults. These may be overwritten by a user config file. + setattr(conf, name, configure.name2format[name][1]) + +DEBUGSHELL = False +if DEBUGSHELL: + from wx.py.crust import CrustFrame + from wx.py.shell import ShellFrame + +# Fldigi XML-RPC control opens a local socket. If socket.setdefaulttimeout() is not +# called, the timeout on Linux is zero (1 msec) and on Windows is 2 seconds. So we +# call it to insure consistent behavior. +import socket +socket.setdefaulttimeout(0.005) + +HAMLIB_DEBUG = 0 + +application = None + +if sys.version_info.major > 2: + Q3StringTypes = str +else: + Q3StringTypes = (str, unicode) + +class StdOutput(wx.Frame): + def __init__(self, app): + self.app = app + self.ctrl = None + self.old_stdout = sys.stdout + self.old_stderr = sys.stderr + self.old_excepthook = sys.excepthook + self.path = os.path.join(app.QuiskFilesDir, "quisk_logfile.txt") + try: + size = os.path.getsize(self.path) + except: + size = 0 + if size > 10000: + try: + fp = open(self.path, 'r') + lines = fp.readlines() + fp.close() + fp = open(self.path, 'w') + for i in range(len(lines) // 2, len(lines)): + fp.write(lines[i]) + fp.close() + except: + pass + try: + self.fp = open(self.path, "a", buffering=1, encoding='utf-8', errors='replace', newline=None) + except: + self.fp = None + sys.stdout = self + sys.stderr = self + sys.excepthook = self.ExceptHook + if self.fp: + self.fp.write("\n\n*** Quisk started on %s at %s\n" % (sys.platform, time.asctime())) + def Create(self, parent): + w = self.app.screen_width * 4 // 10 + h = self.app.screen_height * 4 // 10 + title = "Quisk Log File %s" % self.path + wx.Frame.__init__(self, parent, -1, title, size = (w, h), style=wx.DEFAULT_FRAME_STYLE) + self.ctrl = wx.TextCtrl(self, style=wx.TE_MULTILINE|wx.TE_READONLY|wx.HSCROLL) + self.Bind(wx.EVT_SIZE, self.OnSize) + self.Bind(wx.EVT_CLOSE, self.OnBtnClose) + self.ctrl.write("*** Quisk started on %s at %s\n" % (sys.platform, time.asctime())) + def OnSize(self, event): + event.Skip() + w, h = self.GetClientSize() + self.ctrl.SetSize(w, h) + def OnBtnClose(self, event): + self.Show(False) + def Logfile(self, text): + if self.fp: + self.fp.write(text) + self.fp.write("\n") + def write(self, text): + if self.fp: + self.fp.write(text) + if self.old_stdout: + self.old_stdout.write(text) + if self.ctrl: + self.ctrl.write(text) + def flush(self): + pass + def ExceptHook(self, typ, value, traceb): + self.Show(True) + self.Raise() + self.old_excepthook(typ, value, traceb) + +# Command line parsing: be able to specify the config file. +from optparse import OptionParser +parser = OptionParser() +parser.add_option('-c', '--config', dest='config_file_path', + help='Specify the configuration file path') +parser.add_option('', '--config2', dest='config_file_path2', default='', + help='Specify a second configuration file to read after the first') +parser.add_option('-r', '--radio', dest="radio", default='', + help='Specify the radio to use when starting') +parser.add_option('-a', '--ask', action="store_true", dest='AskMe', default=False, + help='Ask which radio to use when starting') +parser.add_option('', '--local', dest='local_option', default='', + help='Specify a custom option that you have programmed yourself') +argv_options = parser.parse_args()[0] +ConfigPath = argv_options.config_file_path # Get config file path +ConfigPath2 = argv_options.config_file_path2 +LocalOption = argv_options.local_option +if sys.platform == 'win32': + path = os.getenv('HOMEDRIVE', '') + os.getenv('HOMEPATH', '') + for thedir in ("Documents", "My Documents", "Eigene Dateien", "Documenti", "Mine Dokumenter"): + config_dir = os.path.join(path, thedir) + if os.path.isdir(config_dir): + break + else: + config_dir = os.path.join(path, "My Documents") + try: + try: + import winreg as Qwinreg + except ImportError: + import _winreg as Qwinreg + key = Qwinreg.OpenKey(Qwinreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders") + val = Qwinreg.QueryValueEx(key, "Personal") + val = Qwinreg.ExpandEnvironmentStrings(val[0]) + Qwinreg.CloseKey(key) + if os.path.isdir(val): + DefaultConfigDir = val + else: + DefaultConfigDir = config_dir + except: + traceback.print_exc() + DefaultConfigDir = config_dir + if not ConfigPath: + ConfigPath = os.path.join(DefaultConfigDir, "quisk_conf.py") + if not os.path.isfile(ConfigPath): + path = os.path.join(config_dir, "quisk_conf.py") + if os.path.isfile(path): + ConfigPath = path + del config_dir +else: + DefaultConfigDir = os.path.expanduser('~') + if not ConfigPath: + ConfigPath = os.path.join(DefaultConfigDir, ".quisk_conf.py") + +# These FFT sizes have multiple small factors, and are preferred for efficiency. FFT size must be an even number. +fftPreferedSizes = [] +for f2 in range(1, 13): + for y in (1, 3, 5, 7, 9, 11, 13, 15): + for z in (1, 3, 5, 7, 9, 11, 13, 15): + x = 2**f2 * y * z + if 300 <= x <= 5000 and x not in fftPreferedSizes: + fftPreferedSizes.append(x) +fftPreferedSizes.sort() + +def round(x): # round float to nearest integer + if x >= 0: + return int(x + 0.5) + else: + return - int(-x + 0.5) + +def str2freq (freq): + if '.' in freq: + freq = int(float(freq) * 1E6 + 0.1) + else: + freq = int(freq) + return freq + +def get_filter_tx(mode): # Return the bandwidth, center of the Tx filters + if mode in ('LSB', 'USB'): + bw = 2700 + center = 1650 + elif mode in ('CWL', 'CWU'): + bw = 10 + center = 0 + elif mode in ('AM', 'DGT-IQ'): + bw = 6000 + center = 0 + elif mode in ('FM', 'DGT-FM'): + bw = 10000 + center = 0 + elif mode in ('FDV-L', 'FDV-U'): + bw = 2700 + center = 1500 + else: + bw = 2700 + center = 1650 + if mode in ('CWL', 'LSB', 'DGT-L', 'FDV-L'): + center = - center + return bw, center + +Mode2Index = {'CWL':0, 'CWU':1, 'LSB':2, 'USB':3, 'AM':4, 'FM':5, 'EXT':6, 'DGT-U':7, 'DGT-L':8, 'DGT-IQ':9, + 'IMD':10, 'FDV-U':11, 'FDV-L':12, 'DGT-FM':13} + +class Timer: + """Debug: measure and print times every ptime seconds. + + Call with msg == '' to start timer, then with a msg to record the time. + """ + def __init__(self, ptime = 1.0): + self.ptime = ptime # frequency to print in seconds + self.time0 = 0 # time zero; measure from this time + self.time_print = 0 # last time data was printed + self.timers = {} # one timer for each msg + self.names = [] # ordered list of msg + self.heading = 1 # print heading on first use + def __call__(self, msg): + tm = time.time() + if msg: + if not self.time0: # Not recording data + return + if msg in self.timers: + count, average, highest = self.timers[msg] + else: + self.names.append(msg) + count = 0 + average = highest = 0.0 + count += 1 + delta = tm - self.time0 + average += delta + if highest < delta: + highest = delta + self.timers[msg] = (count, average, highest) + if tm - self.time_print > self.ptime: # time to print results + self.time0 = 0 # end data recording, wait for reset + self.time_print = tm + if self.heading: + self.heading = 0 + print ("count, msg, avg, max (msec)") + print("%4d" % count, end=' ') + for msg in self.names: # keep names in order + count, average, highest = self.timers[msg] + if not count: + continue + average /= count + print(" %s %7.3f %7.3f" % (msg, average * 1e3, highest * 1e3), end=' ') + self.timers[msg] = (0, 0.0, 0.0) + print() + else: # reset the time to zero + self.time0 = tm # Start timer + if not self.time_print: + self.time_print = tm + +## T = Timer() # Make a timer instance + +class HamlibHandlerSerial: + "Create a serial port for Hamlib control that emulates the FlexRadio PowerSDR 2.x command set." + # This implements some Kenwood TS-2000 commands, but it is far from complete. + # See http://k5fr.com/binary/CatCommandReferenceGuide.pdf + # Test on linux by setting the Quisk serial port to /tmp/QuiskTTY0 and using putty with font Ubuntu Mono. + # Commands are "ZZAR;" and "ZZAR+030;" - no newline. + + # Additional logic contributed by Dr. Karsten Schmidt + + Mo2CoKen = {'CWL':7, 'CWU':3, 'LSB':1, 'USB':2, 'AM':5, 'FM':4, 'DGT-U':9, 'DGT-L':6, 'DGT-FM':4, 'DGT-IQ':9} + Co2MoKen = {1:'LSB', 2:'USB', 3:'CWU', 4:'FM', 5:'AM', 6:'DGT-L', 7:'CWL', 9:'DGT-U'} + Mo2CoFlex = {'CWL':3, 'CWU':4, 'LSB':0, 'USB':1, 'AM':6, 'FM':5, 'DGT-U':7, 'DGT-L':9, 'DGT-FM':5, 'DGT-IQ':7} + Co2MoFlex = {0:'LSB', 1:'USB', 3:'CWL', 4:'CWU', 5:'FM', 6:'AM', 7:'DGT-U', 9:'DGT-L'} + ConvertZZAC_P1_2_Step = { 0: 1, + 1: 10, + 2: 50, + 3: 100, + 4: 250, + 5: 500, + 6: 1000, + 7: 5000, + 8: 9000, + 9: 10000, + 10: 100000, + 11: 250000, + 12: 500000, + 13: 1000000, + 14: 10000000} + ConvertZZAC_Step_2_P1 = { 1: 0, + 10: 1, + 50: 2, + 100: 3, + 250: 4, + 500: 5, + 1000: 6, + 5000: 7, + 9000: 8, + 10000: 9, + 100000: 10, + 250000: 11, + 500000: 12, + 1000000: 13, + 10000000: 14} + def __init__(self, app, public_name): + self.app = app + self.port = None + self.received = '' + self.radio_id = '019' + self.public_name = public_name # the public name for the serial port + self.tune_step = 1000 + if sys.platform == 'win32' or public_name.startswith("/dev/"): + try: + import serial + except: + print ("Please install the pyserial module.") + else: + try: + self.port = serial.Serial(public_name, timeout=0, write_timeout=0) + if HAMLIB_DEBUG: print ("Open CAT serial port", public_name) + except: + print ("The serial port %s could not be opened." % public_name) + else: + import tty + if os.path.lexists(public_name): + try: + os.remove(public_name) + except: + print ("Can not remove the file", public_name) + try: + self.port, slave = os.openpty() # we are the master device fd, slave is a pseudo tty + tty.setraw(self.port) + tty.setraw(slave) + except: + print ("Can not create the serial port") + self.port = None + else: + try: + os.symlink(os.ttyname(slave), public_name) # create a link from the specified name to the slave device + except: + print ("Can not create a link named", public_name) + self.port = None + else: + if HAMLIB_DEBUG: print ("Create", public_name, "from", os.ttyname(slave)) + def open(self): + return + def close(self): + if sys.platform == 'win32': + if self.port: + self.port.close() + self.port = None + else: + if self.public_name: + try: + os.remove(self.public_name) + except: + pass + def Read(self): + if self.port is None: + return + if sys.platform == 'win32': + text = self.port.read(99) + if not isinstance(text, Q3StringTypes): + text = text.decode('utf-8') + self.received += text + else: + while True: + r, w, x = select.select((self.port,), (), (), 0) + if not r: + break + text = os.read(self.port, 1) + if not isinstance(text, Q3StringTypes): + text = text.decode('utf-8') + self.received += text + def Process(self): + """This is the main processing loop, and is called frequently. It reads and satisfies requests.""" + self.Read() + if ';' in self.received: # A complete command ending with semicolon is available + cmd, self.received = self.received.split(';', 1) # Split off the command, save any further characters + else: + return + cmd = cmd.strip() # Here is our command and data + if cmd[0:2] in ('ZZ', 'zz', 'Zz', 'zZ'): + data = cmd[4:] + cmd = cmd[0:4].upper() + func = cmd + else: + data = cmd[2:] + cmd = cmd[0:2].upper() + if cmd in ('FA', 'FB', 'IF', 'PS'): # Use the ZZxx command method + func = 'ZZ' + cmd + else: # Use the two-letter method + func = cmd + if data: + if HAMLIB_DEBUG: print ("Process command :", cmd, data) + try: + func = getattr(self, func) + except: + print ("Unimplemented serial port function", func, 'cmd', cmd, 'data', data) + self.Write('?;') + return + func(cmd, data, len(data)) + def Error(self, cmd, data): + self.Write('?;') + print ("*** Error for cmd %s data %s" % (cmd, data)) + def Write(self, data): + if HAMLIB_DEBUG: print ("Serial port write:", data) + if self.port is None: + return + if isinstance(data, Q3StringTypes): + data = data.encode('utf-8', errors='ignore') + if sys.platform == 'win32': + self.port.write(data) + else: + r, w, x = select.select((), (self.port,), (), 0) + if w: + os.write(self.port, data) + def set_frequency(self, freq): + tune = freq - self.app.VFO + d = self.app.sample_rate * 45 // 100 + if -d <= tune <= d: # Frequency is on-screen + vfo = self.app.VFO + else: # Change the VFO + vfo = (freq // 5000) * 5000 - 5000 + tune = freq - vfo + self.app.BandFromFreq(freq) + self.app.ChangeHwFrequency(tune, vfo, 'FreqEntry') + if HAMLIB_DEBUG: print("New Freq rx,tx", self.app.txFreq + self.app.VFO, self.app.rxFreq + self.app.VFO) + def ZZAC(self, cmd, data, length): # Set or read tune steps + if length == 0: + xxx = self.ConvertZZAC_Step_2_P1.get(self.tune_step) + self.Write("%s%02d;" % (cmd, xxx)) + elif length == 2: + p1 = int(data, base=10) + try: + self.tune_step = self.ConvertZZAC_P1_2_Step.get(p1) + except: + self.Error(cmd, data) + else: + self.Error(cmd, data) + def ZZAD(self, cmd, data, length): # VFO A down by a selected step + if length == 0: + oldfreq = self.app.rxFreq + self.app.VFO + freq = oldfreq - self.tune_step + self.set_frequency(freq) + else: + self.Error(cmd, data) + def AG(self, cmd, data, length): # audio gain + if length == 1: + self.Write("%s%s120;" % (cmd, data[0])) + def ZZAG(self, cmd, data, length): # audio gain + if length == 0: + n = (self.app.volumeAudio + 5) / 10 + self.Write("%s%03d;" % (cmd, n)) + elif length == 3: + vol = int(data, base=10) * 10 + self.app.sliderVol.SetValue(vol) + self.app.ChangeVolume() + else: + self.Error(cmd, data) + def ZZAR(self, cmd, data, length): # AGC level + if length == 0: + n = self.app.levelAGC * 140 // 1000 - 20 # Convert AGC 0 to 1000 -> -20 to 120 + if n < 0: + self.Write("%s-%03d;" % (cmd, - n)) + else: + self.Write("%s+%03d;" % (cmd, n)) + elif length == 4: + val = int(data, base=10) + val = (val + 20) * 1000 // 140 # Convert AGC -20 to 120 -> 0 to 1000 + self.app.SliderAGC.SetSlider(value_on=val) + self.app.BtnAGC.SetValue(True, True) + else: + self.Error(cmd, data) + def ZZAU(self, cmd, data, length): # VFO A up by a selected step + if length == 0: + oldfreq = self.app.rxFreq + self.app.VFO + freq = oldfreq + self.tune_step + self.set_frequency(freq) + else: + self.Error(cmd, data) + def ZZBS(self, cmd, data, length): # band switch + if length == 0: + band = self.app.lastBand + try: + band = int(band, base=10) + self.Write("%s%03d;" % (cmd, band)) + except: + self.Write("%s888;" % cmd) + elif length == 3: + band = data + if band[0] == '0': + band = band[1:] + if band[0] == '0': + band = band[1:] + try: + self.app.bandBtnGroup.SetLabel(band, do_cmd=True, direction=0) + except: + self.Error(cmd, data) + else: + self.Error(cmd, data) + def ZZAI(self, cmd, data, length): # broadcast changes + if length == 0: + self.Write("%s0;" % cmd) + elif length == 1 and data[0] == '0': + pass + else: + self.Error(cmd, data) + def ZZFA(self, cmd, data, length): # frequency of VFO A, the receive frequency + if length == 0: + self.Write("%s%011d;" % (cmd, self.app.rxFreq + self.app.VFO)) + elif length == 11: + freq = int(data, base=10) + self.set_frequency(freq) + else: + self.Error(cmd, data) + def ZZFB(self, cmd, data, length): # frequency of VFO B + if length == 0: + self.Write("%s%011d;" % (cmd, self.app.txFreq + self.app.VFO)) + elif length == 11: + freq = int(data, base=10) + tune = freq - self.app.VFO + d = self.app.sample_rate * 45 // 100 + if -d <= tune <= d: # Frequency is on-screen + vfo = self.app.VFO + else: # Change the VFO + vfo = (freq // 5000) * 5000 - 5000 + tune = freq - vfo + self.app.BandFromFreq(freq) + self.app.ChangeHwFrequency(tune, vfo, 'FreqEntry') + else: + self.Error(cmd, data) + def FR(self, cmd, data, length): # receive VFO is always VFO A + if length == 0: + self.Write("%s0;" % cmd) + elif length == 1 and data[0] == '0': + pass + else: + self.Error(cmd, data) + def FT(self, cmd, data, length): # transmit VFO + if self.app.split_rxtx: + vfo = '1' + else: + vfo = '0' + if length == 0: + self.Write("%s%s;" % (cmd, vfo)) + elif length == 1 and data[0] == vfo: + pass + else: + self.Error(cmd, data) + def ID(self, cmd, data, length): # return radio ID + if length == 0: + self.Write('%s%s;' % (cmd, self.radio_id)) + else: + self.Error(cmd, data) + def ZZID(self, cmd, data, length): # set radio id to Flex + if length == 0: + self.radio_id = '900' + else: + self.Error(cmd, data) + def ZZIF(self, cmd, data, length): # return information for ZZIF and IF + ritFreq = self.app.ritScale.GetValue() + if self.app.ritButton.GetValue(): + rit = 1 + else: + rit = 0 + mode = self.app.mode + info = cmd + info += "%011d" % (self.app.rxFreq + self.app.VFO) # frequency, ZZFA + info += '0000' + if ritFreq < 0: # RIT freq + info += "-%05d" % -ritFreq + else: + info += "+%05d" % ritFreq + info += "%d" % rit # RIT status + info += '0000' + if QS.is_key_down(): # MOX, key down + info += '1' + else: + info += '0' + if len(cmd) == 4: # Flex ZZIF + code = self.Mo2CoFlex.get(mode, 1) + info += "%02d" % code # operating mode + else: # Kenwood IF + code = self.Mo2CoKen.get(mode, 1) + info += "%d" % code # operating mode + info += '00' + if self.app.split_rxtx: # VFO split status + info += '1' + else: + info += '0' + info += '0000' + info += ';' + self.Write(info) + def MD(self, cmd, data, length): # the mode; USB, CW, etc. + if length == 0: + mode = self.app.mode + code = self.Mo2CoKen.get(mode, 2) + self.Write("%s%d;" % (cmd, code)) + elif length == 1: + code = int(data, base=10) + mode = self.Co2MoKen.get(code, 'USB') + self.app.modeButns.SetLabel(mode, True) # Set mode + else: + self.Error(cmd, data) + def ZZMD(self, cmd, data, length): # the mode; USB, CW, etc. + if length == 0: + mode = self.app.mode + code = self.Mo2CoFlex.get(mode, 1) + self.Write("%s%02d;" % (cmd, code)) + elif length == 2: + code = int(data, base=10) + mode = self.Co2MoFlex.get(code, 'USB') + self.app.modeButns.SetLabel(mode, True) # Set mode + else: + self.Error(cmd, data) + def ZZMU(self, cmd, data, length): # MultiRx on/off + if length == 0: + self.Write("%s0;" % cmd) + def OI(self, cmd, data, length): # return information + self.ZZIF(cmd, data, length) + def ZZPS(self, cmd, data, length): # power status + if length == 0: + self.Write("%s1;" % cmd) + def ZZRS(self, cmd, data, length): # the RX2 status + if length == 0: + self.Write("%s0;" % cmd) + elif length == 1 and data[0] == '0': + pass + else: + self.Error(cmd, data) + def RX(self, cmd, data, length): # turn off MOX + if length == 0: + self.app.pttButton.SetValue(0, True) + else: + self.Error(cmd, data) + def ZZSP(self, cmd, data, length): # the split status + if length == 0: + if self.app.split_rxtx: + self.Write("%s1;" % cmd) + else: + self.Write("%s0;" % cmd) + else: + self.Error(cmd, data) + def ZZSW(self, cmd, data, length): # transmit VFO is A or B + if length == 0: + if self.app.split_rxtx: + self.Write("%s1;" % cmd) + else: + self.Write("%s0;" % cmd) + def TX(self, cmd, data, length): # turn on MOX + if length == 0: + self.app.pttButton.SetValue(1, True) + else: + self.Error(cmd, data) + def ZZTX(self, cmd, data, length): # the MOX status + if length == 0: + if QS.is_key_down(): + self.Write("%s1;" % cmd) + else: + self.Write("%s0;" % cmd) + elif length == 1: + if data[0] == '0': + self.app.pttButton.SetValue(0, True) + else: + self.app.pttButton.SetValue(1, True) + else: + self.Error(cmd, data) + def ZZVE(self, cmd, data, length): # is VOX enabled + if length == 0: + if self.app.useVOX: + self.Write("%s1;" % cmd) + else: + self.Write("%s0;" % cmd) + else: + self.Error(cmd, data) + def XT(self, cmd, data, length): # the XIT + if length == 0: + self.Write("%s0;" % cmd) + elif length == 1 and data[0] == '0': + pass + else: + self.Error(cmd, data) + +class HamlibHandlerRig2: # Test with telnet localhost 4532 + """This class is created for each connection to the server. It services requests from each client.""" + SingleLetters = { # convert single-letter commands to long commands + '_':'info', + 'f':'freq', + 'i':'split_freq', + 'l':'level', + 'm':'mode', + 'p':'parm', + 's':'split_vfo', + 't':'ptt', + 'u':'func', + 'v':'vfo', + } + def __init__(self, app, sock, address): + self.app = app # Reference back to the "hardware" + self.sock = sock + sock.settimeout(0.0) + self.address = address + self.params = '' # params is the string following the command + self.received = '' + self.input = '' + self.vfo = "Main" + self.split_mode = 0 + self.split_vfo = 'VFO' + h = self.Handlers = {} + h[''] = self.ErrProtocol + h['dump_state'] = self.DumpState + h['chk_vfo'] = self.ChkVfo # Thanks to Franco Spinelli, IW2DHW + h['get_freq'] = self.GetFreq + h['set_freq'] = self.SetFreq + h['get_info'] = self.GetInfo + h['get_mode'] = self.GetMode + h['set_mode'] = self.SetMode + h['get_vfo'] = self.GetVfo + h['set_vfo'] = self.SetVfo + h['get_func'] = self.GetFunc + h['set_func'] = self.SetFunc + h['get_level'] = self.GetLevel + h['set_level'] = self.SetLevel + h['get_parm'] = self.GetParm + h['set_parm'] = self.SetParm + h['get_ptt'] = self.GetPtt + h['set_ptt'] = self.SetPtt + h['get_split_freq'] = self.GetSplitFreq + h['set_split_freq'] = self.SetSplitFreq + h['get_split_vfo'] = self.GetSplitVfo + h['set_split_vfo'] = self.SetSplitVfo + self.MakeDumpState() + def MakeDumpState(self): + dump_state = [] + # rigctld protocol version + dump_state.append( b"0" ) + # rig model + dump_state.append( b"2" ) + # ITU region + dump_state.append( b"1" ) # Europe, Africa and Northern Asia + #dump_state.append( b"2" ) # North America, South America and Greenland + #dump_state.append( b"3" ) # South Pacific and Southern Asia + # RX frequency ranges + # start, end, modes, low_power, high_power, vfo, ant + #dump_state.append( b"150000.000000 1500000000.000000 0x1ff -1 -1 0x10000003 0x3" ) + # AM | CW | USB | LSB | NFM | WFM | DSB + dump_state.append( b"100000.000000 6000000000.000000 0x8006f -1 -1 0x4000000 0x3" ) # HackRF (100kHz - 6GHz), VFO:Main + # End of RX frequency ranges + dump_state.append( b"0 0 0 0 0 0 0" ) + # TX frequency ranges + # start, end, modes, low_power, high_power, vfo, ant + dump_state.append( b"100000.000000 6000000000.000000 0x8006f -1 -1 0x4000000 0x3" ) # HackRF (100kHz - 6GHz), VFO:Main + # End of TX frequency ranges + dump_state.append( b"0 0 0 0 0 0 0" ) + # Tuning steps + # mode, tuning_step + dump_state.append( b"0x1ff 1" ) + dump_state.append( b"0x1ff 0" ) + # End of tuning steps + dump_state.append( b"0 0" ) + # Filter sizes + # mode, width + dump_state.append( b"0x1e 2400" ) + dump_state.append( b"0x2 500" ) + dump_state.append( b"0x1 8000" ) + dump_state.append( b"0x1 2400" ) + dump_state.append( b"0x20 15000" ) + dump_state.append( b"0x20 8000" ) + dump_state.append( b"0x40 230000" ) + # End of filter sizes + dump_state.append( b"0 0" ) + # ------ ??? ------ + # max_rit + dump_state.append( b"9990" ) + # max_xit + dump_state.append( b"9990" ) + # max_ifshift + dump_state.append( b"10000" ) + # announces + dump_state.append( b"0" ) + # MAXDBLSTSIZ preamp + dump_state.append( b"10" ) + # MAXDBLSTSIZ attenuator + dump_state.append( b"10 20 30" ) + # has_get_func + dump_state.append( b"0x3effffff" ) + # has_set_func + dump_state.append( b"0x3effffff" ) + # has_get_level + dump_state.append( b"0xfffffffff7ffffff" ) + # has_set_level + dump_state.append( b"0xffffffff83ffffff" ) + # has_get_parm + dump_state.append( b"0xffffffffffffffff" ) + # has_set_parm + dump_state.append( b"0xffffffffffffffbf" ) + # END + self.dump_state = b'\n'.join(dump_state) + self.dump_state += b'\n' + def Send(self, text): + """Send text back to the client.""" + if isinstance(text, Q3StringTypes): + text = text.encode('utf-8', errors='ignore') + try: + self.sock.sendall(text) + except socket.error: + self.sock.close() + self.sock = None + def Reply(self, *args): # args is name, value, name, value, ..., int + """Create a string reply of name, value pairs, and an ending integer code.""" + if self.extended: # Use extended format; echo the command and parameters. + t = "%s:" % self.command + if self.params: + t += " %s" % self.params + t += self.extended + for i in range(0, len(args) - 1, 2): + t = "%s%s: %s%c" % (t, args[i], args[i+1], self.extended) + t += "RPRT %d\n" % args[-1] + elif len(args) > 1: # Use simple format + t = '' + for i in range(1, len(args) - 1, 2): + t = "%s%s\n" % (t, args[i]) + else: # No names; just the required integer code + t = "RPRT %d\n" % args[0] + self.Send(t) + def Reply2(self, text, code): + """Create a reply string in a different format.""" + if self.extended: # Use extended format + t = "%s: %s%s%s\nRPRT %d\n" % (self.command, self.params, self.extended, text, code) + self.Send(t) + else: + if self.command[0:4] == "set_": + self.Send("%s\nRPRT %d\n" % (text, code)) + else: + self.Send("%s\n" % text) + def ErrParam(self): # Invalid parameter + self.input = '' + self.Reply(-1) + def UnImplemented(self): # Command not implemented + self.input = '' + self.Reply(-4) + def ErrProtocol(self): # Protocol error + self.input = '' + self.Reply(-8) + def GetParamNumber(self): + if not self.input: + return None + number = '' + for i in range(len(self.input)): + ch = self.input[i] + if ch in "+-.0123456789": + number = number + ch + else: + self.input = self.input[i:].strip() + break + else: + self.input = '' + if self.params: + self.params = self.params + ' ' + number + else: + self.params = number + try: + if '.' in number: + return float(number) + else: + return int(number) + except: + return None + def GetParamName(self): + if not self.input: + return '' + if self.input[0] == '?': + self.params = '?' + self.input = self.input[1:].strip() + return '?' + name = '' + for i in range(len(self.input)): + ch = self.input[i] + if ch in string.ascii_letters or ch in "_-": + name = name + ch + else: + self.input = self.input[i:].strip() + break + else: + self.input = '' + if self.params: + self.params = self.params + ' ' + name + else: + self.params = name + return name + def Process(self): + """This is the main processing loop, and is called frequently. It reads and satisfies requests.""" + if not self.sock: + return 0 + try: # Read any data from the socket + text = self.sock.recv(1024) + except socket.timeout: # This does not work + pass + except socket.error: # Nothing to read + pass + else: # We got some characters + if not isinstance(text, Q3StringTypes): + text = text.decode('utf-8') + self.received += text + if '\n' in self.received: # A complete command ending with newline is available + self.input, self.received = self.received.split('\n', 1) # Split off the command, save any further characters + else: + return 1 + self.input = self.input.strip() # Here is our command line + while self.input: + # Parse the commands and call the appropriate handlers + self.params = '' + if self.input[0] == '+': # rigctld Extended Response Protocol + self.extended = '\n' + self.input = self.input[1:].strip() + elif self.input[0] in ';|,': # rigctld Extended Response Protocol + self.extended = self.input[0] + self.input = self.input[1:].strip() + else: + self.extended = None + letter = self.input[0:1] + if letter == '\\': # long form command starting with backslash + args = self.input[1:].split(None, 1) + self.command = args[0] + if len(args) == 1: + self.input = '' + else: + self.input = args[1].strip() + else: # single-letter commands + self.input = self.input[1:].strip() + if letter in 'Qq': # Quit command + self.sock.close() + self.input = '' + return 0 + try: + command = self.SingleLetters[letter.lower()] + except KeyError: + self.UnImplemented() + return 1 + else: + if letter in string.ascii_uppercase: + self.command = 'set_' + command + else: + self.command = 'get_' + command + self.Handlers.get(self.command, self.UnImplemented)() + return 1 + # These are the handlers for each request + def DumpState(self): + self.Send(self.dump_state) + def ChkVfo(self): + if self.extended: + self.Send('ChkVFO: 0\n') + else: + self.Send('0\n') + def GetFreq(self): # The Rx frequency + rx = self.app.multi_rx_screen.receiver_list + if rx: + rx = rx[0] + self.Reply('Frequency', rx.txFreq + rx.VFO, 0) + else: + self.Reply('Frequency', self.app.rxFreq + self.app.VFO, 0) + def SetFreq(self): # The Rx frequency + freq = self.GetParamNumber() + try: + freq = float(freq) + self.Reply(0) + except: + self.ErrParam() + else: + freq = int(freq + 0.5) + # For multiple receivers this controls the first added receiver frequency; else it controls split Rx/Tx frequency. + rx = self.app.multi_rx_screen.receiver_list + if rx: + rx[0].ChangeRxTxFrequency(freq) + else: + self.app.ChangeRxTxFrequency(freq, None) + def GetSplitFreq(self): # The Tx Frequency + self.Reply('TX Frequency', self.app.txFreq + self.app.VFO, 0) + def SetSplitFreq(self): # The Tx Frequency + freq = self.GetParamNumber() + try: + freq = float(freq) + self.Reply(0) + except: + self.ErrParam() + else: + freq = int(freq + 0.5) + # This always controls the Tx frequency + self.app.ChangeRxTxFrequency(None, freq) + def GetSplitVfo(self): + self.Reply('Split', self.split_mode, 'TX VFO', self.split_vfo, 0) + def SetSplitVfo(self): + split = self.GetParamNumber() + vfo = self.GetParamName() + try: + split = int(split) + self.Reply(0) + except: + # traceback.print_exc() + self.ErrParam() + else: + self.split_mode = split + if split: + self.split_vfo = vfo + else: + self.split_vfo = 'VFO' + # If there are no multi receivers, this sets split Rx/Tx mode. + if not self.app.multi_rx_screen.receiver_list: + self.app.splitButton.SetValue(split, True) + def GetInfo(self): + self.Reply("Info", self.app.main_frame.title, 0) + def GetMode(self): + mode = self.app.mode + if mode == 'CWU': + mode = 'CW' + elif mode == 'CWL': # Is this what CWR means? + mode = 'CWR' + elif mode == 'DGT-U': + mode = 'PKTUSB' + elif mode == 'DGT-L': + mode = 'PKTLSB' + elif mode == 'DGT-FM': + mode = 'PKTFM' + elif mode[0:4] == 'DGT-IQ': + mode = 'PKTUSB' + self.Reply('Mode', mode, 'Passband', self.app.filter_bandwidth, 0) + def SetMode(self): + mode = self.GetParamName() + bw = self.GetParamNumber() + if mode == '?': # send back supported parameters + self.Reply2('USB LSB AM FM CW CWR PKTUSB PKTLSB PKTFM', 0) + return + try: + bw = int(bw) + except: + self.ErrParam() + return + if mode in ('USB', 'LSB', 'AM', 'FM'): + self.Reply(0) + elif mode[0:4] == 'DGT-': + self.Reply(0) + elif mode == 'PKTUSB': + mode = 'DGT-U' + self.Reply(0) + elif mode == 'PKTLSB': + mode = 'DGT-L' + self.Reply(0) + elif mode == 'PKTFM': + mode = 'DGT-FM' + self.Reply(0) + elif mode == 'CW': + mode = 'CWU' + self.Reply(0) + elif mode == 'CWR': + mode = 'CWL' + self.Reply(0) + else: + self.ErrParam() + return + self.app.modeButns.SetLabel(mode, True) # Set mode + if bw <= 0: # use default bandwidth + return + # Choose button closest to requested bandwidth + buttons = self.app.filterButns.GetButtons() + Lab = buttons[0].GetLabel() + diff = abs(int(Lab) - bw) + for i in range(1, len(buttons) - 1): + label = buttons[i].GetLabel() + df = abs(int(label) - bw) + if df < diff: + Lab = label + diff = df + self.app.filterButns.SetLabel(Lab, True) + def GetVfo(self): + self.Reply('VFO', self.vfo, 0) + def SetVfo(self): + name = self.GetParamName() + if name == '?': # send back supported parameters + self.Reply2('Main', 0) + else: + self.vfo = name + self.Reply(0) + def GetPtt(self): + if QS.is_key_down(): + self.Reply('PTT', 1, 0) + else: + self.Reply('PTT', 0, 0) + def SetPtt(self): + ptt = self.GetParamNumber() + try: + ptt = int(ptt) + self.Reply(0) + except: + self.ErrParam() + else: + self.app.pttButton.SetValue(ptt, True) + def GetFunc(self): + name = self.GetParamName() + if name == '?': # send back supported functions + self.Reply2('TUNER', 0) + elif name == 'TUNER': + if self.app.spotButton.GetValue(): + self.Reply2(1, 0) + else: + self.Reply2(0, 0) + else: + self.ErrParam() + def SetFunc(self): + name = self.GetParamName() + value = self.GetParamNumber() + if name == '?': # send back supported functions + self.Reply2('TUNER', 0) + elif name == 'TUNER': + try: + value = int(value) + except: + self.ErrParam() + else: + self.Reply(0) + if value: + self.app.spotButton.SetValue(1, True) + else: + self.app.spotButton.SetValue(0, True) + else: + self.ErrParam() + def GetLevel(self): + name = self.GetParamName() + if name == '?': # send back supported parameters + self.Reply2('AGC AF', 0) + elif name == 'AGC': + if self.app.BtnAGC.GetValue(): + self.Reply2(4, 0) + else: + self.Reply2(0, 0) + elif name == 'AF': + x = self.app.audio_volume # audio_volume is 0 to 1.000 + self.Reply2("%.5f" % x, 0) + else: + self.ErrParam() + def SetLevel(self): + name = self.GetParamName() + value = self.GetParamNumber() + if name == '?': # send back supported parameters + self.Reply2('AGC AF', 0) + elif name == 'AGC': + try: + value = int(value) + except: + self.ErrParam() + else: + self.Reply(0) + if value: + self.app.BtnAGC.SetValue(1, True) + else: + self.app.BtnAGC.SetValue(0, True) + elif name == 'AF': + try: + value = float(value) + except: + self.ErrParam() + else: + self.Reply(0) + v = 1000.0 * math.log10(49.0 * value + 1) / math.log10(50) + self.app.sliderVol.SetValue(int(v + 0.5)) + self.app.ChangeVolume() + else: + self.ErrParam() + def GetParm(self): + name = self.GetParamName() + if name == '?': # send back supported parameters + self.Reply2('', 0) + else: + self.ErrParam() + def SetParm(self): + name = self.GetParamName() + if name == '?': # send back supported parameters + self.Reply2('', 0) + else: + self.ErrParam() + +class SoundThread(threading.Thread): + """Create a second (non-GUI) thread to read, process and play sound.""" + def __init__(self, samples_from_python): + self.samples_from_python = samples_from_python + self.do_init = 1 + threading.Thread.__init__(self, name="QuiskSound") + self.doQuit = threading.Event() + self.doQuit.clear() + #self.time0 = time.time() + if hasattr(Hardware, "PollCwKey"): + self.poll_cw = True + else: + self.poll_cw = False + def run(self): + """Read, process, play sound; then notify the GUI thread to check for FFT data.""" + if self.do_init: # Open sound using this thread + self.do_init = 0 + QS.start_sound() + wx.CallAfter(application.PostStartup) + while not self.doQuit.is_set(): + #tm = time.time() + #print ("Quisk thread %12.3f" % ((tm - self.time0) * 1E3)) + #self.time0 = tm + if self.samples_from_python: + samples = Hardware.GetRxSamples() + if samples: + QS.set_params(rx_samples=samples) + if self.poll_cw: + Hardware.PollCwKey() + if conf.midi_cwkey_device: + byts = QS.control_midi(get_event=1) + if byts: + wx.CallAfter(application.OnReadMIDI, byts) + application.wdsp.control() + QS.read_sound() + wx.CallAfter(application.OnReadSound) + #if sys.platform == 'win32': + # time.sleep(0.000) + QS.control_midi(close_port=1) + QS.close_sound() + def stop(self): + """Set a flag to indicate that the sound thread should end.""" + self.doQuit.set() + +class ConfigScreen(wx.Panel): + """Display a notebook with status and configuration data""" + def __init__(self, parent, width, fft_size): + self.y_scale = 0 + self.y_zero = 0 + self.zoom_control = 0 + self.finish_pages = True + self.width = width + wx.Panel.__init__(self, parent) + self.notebook = notebook = wx.Notebook(self) + font = wx.Font(conf.config_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL, + wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + notebook.SetFont(font) + sizer = wx.BoxSizer() + sizer.Add(notebook, 1, wx.EXPAND) + self.SetSizer(sizer) + # create the page windows + self.status = ConfigStatus(notebook, width, fft_size) + self.SetBackgroundColour(self.status.bg_color) + self.SetForegroundColour(self.status.tfg_color) + notebook.bg_color = self.status.bg_color + notebook.tfg_color = self.status.tfg_color + notebook.AddPage(self.status, "Status") + self.config = configure.ConfigConfig(notebook, width) + notebook.AddPage(self.config, "Config") + self.favorites = ConfigFavorites(notebook, width) + notebook.AddPage(self.favorites, "Favorites") + tx_audio = configure.ConfigTxAudio(notebook) + notebook.AddPage(tx_audio, "Tx Audio") + tx_audio.status = self.status + def FinishPages(self): + if self.finish_pages: + self.finish_pages = False + application.local_conf.AddPages(self.notebook, self.width) + def ChangeYscale(self, y_scale): + pass + def ChangeYzero(self, y_zero): + pass + def OnIdle(self, event): + pass + def SetTxFreq(self, tx_freq, rx_freq): + pass + def OnGraphData(self, data=None): + self.status.OnGraphData(data) + def InitBitmap(self): # Initial construction of bitmap + self.status.InitBitmap() + +class ConfigStatus(wx.ScrolledCanvas): + """Display the status screen.""" + def __init__(self, parent, width, fft_size): + wx.ScrolledCanvas.__init__(self, parent) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.bg_color = self.GetBackgroundColour() + try: + va = wx.StaticText.GetClassDefaultAttributes() + self.tfg_color = wx.Colour(va.colFg) # use for text foreground + except: + self.tfg_color = self.GetForegroundColour() + self.width = width + self.fft_size = fft_size + self.scroll_height = None + self.interrupts = 0 + self.read_error = -1 + self.write_error = -1 + self.underrun_error = -1 + self.hl2_txbuf_errors = 0 + self.fft_error = -1 + self.latencyCapt = -1 + self.latencyPlay = -1 + self.y_scale = 0 + self.y_zero = 0 + self.zoom_control = 0 + self.rate_min = -1 + self.rate_max = -1 + self.chan_min = -1 + self.chan_max = -1 + self.mic_max_display = -90.0 + self.err_msg = "No response" + self.msg1 = "" + self.font = wx.Font(conf.status_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL, + wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + if wxVersion in ('2', '3'): + self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) + else: + self.SetBackgroundStyle(wx.BG_STYLE_PAINT) + self.SetFont(self.font) + charx = self.charx = self.GetCharWidth() + chary = self.chary = self.GetCharHeight() + self.dy = chary # line spacing + self.rjustify1 = (0, 1, 0) + self.tabstops1 = [0] * 3 + self.tabstops1[0] = x = charx + self.tabstops1[1] = x = x + self.GetTextExtent("FFT number of errors 1234567890")[0] + self.tabstops1[2] = x = x + self.GetTextExtent("XXXX")[0] + self.rjustify2 = (0, 0, 1, 1, 1, 1) + self.tabstops2 = [] + def MakeTabstops(self): + luse = lname = 0 + for use, name, rate, latency, errors, level, dev_errmsg in QS.sound_errors(): + name = self.TrimName(name) + w, h = self.GetTextExtent(use) + luse = max(luse, w) + w, h = self.GetTextExtent(name) + lname = max(lname, w) + if luse == 0: + return + charx = self.charx + self.tabstops2 = [0] * 6 + self.tabstops2[0] = x = charx + self.tabstops2[1] = x = x + luse + charx * 6 + self.tabstops2[2] = x = x + lname + self.GetTextExtent("Sample rateXXXXXX")[0] + self.tabstops2[3] = x = x + charx * 12 + self.tabstops2[4] = x = x + charx * 12 + self.tabstops2[5] = x = x + charx * 12 + def TrimName(self, name): + if len(name) > 50: + name = name[0:30] + '|||' + name[-17:] + return name + def OnPaint(self, event): + # Make and blit variable data + self.MakeBitmap() + dc = wx.AutoBufferedPaintDC(self) + x, y = self.GetViewStart() + dc.Blit(0, 0, self.mem_width, self.mem_height, self.mem_dc, x, y) + def MakeRow2(self, *args): + for col in range(len(args)): + t = args[col] + if t is None: + continue + if not isinstance(t, Q3StringTypes): + t = str(t) + x = self.tabstops[col] + if self.rjustify[col]: + w, h = self.mem_dc.GetTextExtent(t) + x -= w + if ("Error" in t or "Stream error" in t) and t != "Errors": + self.mem_dc.SetTextForeground('Red') + self.mem_dc.DrawText(t, x, self.mem_y) + self.mem_dc.SetTextForeground(self.tfg_color) + else: + self.mem_dc.DrawText(t, x, self.mem_y) + self.mem_y += self.dy + def InitBitmap(self): # Initial construction of bitmap + self.mem_height = application.screen_height + self.mem_width = application.screen_width + self.bitmap = EmptyBitmap(self.mem_width, self.mem_height) + self.mem_dc = wx.MemoryDC() + self.mem_rect = wx.Rect(0, 0, self.mem_width, self.mem_height) + self.mem_dc.SelectObject(self.bitmap) + br = wx.Brush(self.bg_color) + self.mem_dc.SetBackground(br) + self.mem_dc.SetFont(self.font) + self.mem_dc.SetTextForeground(self.tfg_color) + self.mem_dc.Clear() + def MakeBitmap(self): + self.mem_dc.Clear() + self.mem_y = self.charx + self.tabstops = self.tabstops1 + self.rjustify = self.rjustify1 + if conf.config_file_exists: + cfile = "Configuration file: %s" % conf.config_file_path + else: + cfile = "Configuration file: None" + qfile = "Quisk user files are in %s" % application.QuiskFilesDir + level = "%3.0f" % self.mic_max_display + if self.err_msg: + err_msg = self.err_msg + else: + err_msg = None + self.MakeRow2("Sample interrupts", self.interrupts, cfile) + self.MakeRow2("Microphone or DGT level dB", level, qfile) + self.MakeRow2("FFT number of points", self.fft_size, err_msg) + if conf.dxClHost: # connection to dx cluster + nSpots = len(application.dxCluster.dxSpots) + if nSpots > 0: + msg = str(nSpots) + ' DX spot' + ('' if nSpots==1 else 's') + ' received from ' + application.dxCluster.getHost() + else: + msg = "No DX Cluster data from %s" % conf.dxClHost + self.MakeRow2("FFT number of errors", self.fft_error, msg) + else: + self.MakeRow2("FFT number of errors", self.fft_error) + if conf.use_rx_udp == 10: # Hermes UDP protocol + self.MakeRow2("Hermes-Lite2 Tx buffer errors", self.hl2_txbuf_errors) + self.mem_y += self.dy + if not self.tabstops2: + return + self.tabstops = self.tabstops2 + self.rjustify = self.rjustify2 + self.font.SetUnderlined(True) + self.mem_dc.SetFont(self.font) + self.MakeRow2("Device", "Name", "Sample rate", "Latency", "Errors", "Level dB") + self.font.SetUnderlined(False) + self.mem_dc.SetFont(self.font) + self.mem_y += self.dy * 3 // 10 + if conf.use_sdriq: + self.MakeRow2("Capture radio samples", "SDR-IQ", application.sample_rate, self.latencyCapt, self.read_error) + elif conf.use_rx_udp: + self.MakeRow2("Capture radio samples", "UDP", application.sample_rate, self.latencyCapt, self.read_error) + elif conf.use_soapy: + self.MakeRow2("Capture radio samples", "SoapySDR", application.sample_rate, self.latencyCapt, self.read_error) + for use, name, rate, latency, errors, level, dev_errmsg in QS.sound_errors(): + level = math.sqrt(level) / 2**31 + if level < 1.1E-5: + level = " - " + else: + level = 20 * math.log10(level) + level = "%.2f" % level + self.MakeRow2(use, self.TrimName(name), rate, latency, errors, level) + if dev_errmsg: + x = self.tabstops[0] + self.charx * 4 + self.mem_dc.SetTextForeground('Red') + self.mem_dc.DrawText(dev_errmsg, x, self.mem_y) + self.mem_dc.SetTextForeground(self.tfg_color) + self.mem_y += self.dy + if self.scroll_height is None: + self.scroll_height = self.mem_y + self.dy + self.SetScrollbars(1, 1, 100, self.scroll_height) + def OnGraphData(self, data=None): + if not self.tabstops2: # Must wait for sound to start + self.MakeTabstops() + (self.rate_min, self.rate_max, sample_rate, self.chan_min, self.chan_max, + self.msg1, self.unused, self.err_msg, + self.read_error, self.write_error, self.underrun_error, + self.latencyCapt, self.latencyPlay, self.interrupts, self.fft_error, self.mic_max_display, + self.data_poll_usec + ) = QS.get_state() + self.mic_max_display = 20.0 * math.log10((self.mic_max_display + 1) / 32767.0) + if conf.use_rx_udp == 10: # Hermes UDP protocol + self.hl2_txbuf_errors = QS.get_params("hl2_txbuf_errors") + self.RefreshRect(self.mem_rect) + +class ConfigFavorites(wx.grid.Grid): + def __init__(self, parent, width): + wx.grid.Grid.__init__(self, parent) + self.changed = False + self.RepeaterDict = {} + font = wx.Font(conf.favorites_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL, + wx.FONTWEIGHT_BOLD, False, conf.quisk_typeface) + self.SetFont(font) + self.SetBackgroundColour(parent.bg_color) + self.SetLabelFont(font) + font = wx.Font(conf.favorites_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL, + wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + self.SetDefaultCellFont(font) + self.SetDefaultRowSize(self.GetCharHeight()+3) + self.Bind(wx.grid.EVT_GRID_LABEL_RIGHT_CLICK, self.OnRightClickLabel) + self.Bind(wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self.OnLeftClickLabel) + if wxVersion in ('2', '3'): + self.Bind(wx.grid.EVT_GRID_CELL_CHANGE, self.OnChange) # wxPython 3 + else: + self.Bind(wx.grid.EVT_GRID_CELL_CHANGED, self.OnChange) # wxPython 4 + self.Bind(wx.grid.EVT_GRID_LABEL_LEFT_DCLICK, self.OnLeftDClick) + self.CreateGrid(0, 6) + self.EnableDragRowSize(False) + w = self.GetTextExtent(' 999 ')[0] + self.SetRowLabelSize(w) + self.SetColLabelValue(0, 'Name') + self.SetColLabelValue(1, 'Freq MHz') + self.SetColLabelValue(2, 'Mode') # This column has a choice editor + self.SetColLabelValue(3, 'Description') + self.SetColLabelValue(4, 'Offset kHz') + self.SetColLabelValue(5, 'Tone Hz') + w = self.GetTextExtent("xFrequencyx")[0] + self.SetColSize(0, w * 3 // 2) + self.SetColSize(1, w) + self.SetColSize(4, w) + self.SetColSize(5, w) + self.SetColSize(2, w) + ww = width - w * 7 - self.GetRowLabelSize() - 20 + if ww < w: + ww = w + self.SetColSize(3, ww) + if conf.favorites_file_path: + self.init_path = conf.favorites_file_path + else: + self.init_path = os.path.join(os.path.dirname(ConfigPath), 'quisk_favorites.txt') + conf.favorites_file_in_use = self.init_path + self.ReadIn() + if self.GetNumberRows() < 1: + self.AppendRows() + # Make a popup menu + self.popupmenu = wx.Menu() + item = self.popupmenu.Append(-1, 'Tune to') + self.Bind(wx.EVT_MENU, self.OnPopupTuneto, item) + self.popupmenu.AppendSeparator() + item = self.popupmenu.Append(-1, 'Append') + self.Bind(wx.EVT_MENU, self.OnPopupAppend, item) + item = self.popupmenu.Append(-1, 'Insert') + self.Bind(wx.EVT_MENU, self.OnPopupInsert, item) + item = self.popupmenu.Append(-1, 'Delete') + self.Bind(wx.EVT_MENU, self.OnPopupDelete, item) + self.popupmenu.AppendSeparator() + item = self.popupmenu.Append(-1, 'Move Up') + self.Bind(wx.EVT_MENU, self.OnPopupMoveUp, item) + item = self.popupmenu.Append(-1, 'Move Down') + self.Bind(wx.EVT_MENU, self.OnPopupMoveDown, item) + # Make a timer + self.timer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.OnTimer) + def SetModeEditor(self, mode_names): + self.mode_names = mode_names + for row in range(self.GetNumberRows()): + self.SetCellEditor(row, 2, wx.grid.GridCellChoiceEditor(mode_names, True)) + def FormatFloat(self, freq): + freq = "%.6f" % freq + for i in range(3): + if freq[-1] == '0': + freq = freq[:-1] + else: + break + return freq + def ReadIn(self): + try: + fp = open(self.init_path, 'r') + lines = fp.readlines() + fp.close() + except: + lines = ("my net|7210000|LSB|My net 2030 UTC every Thursday", + "10m FM 1|29.620|FM|Fm local 10 meter repeater") + for row in range(len(lines)): + self.AppendRows() + fields = lines[row].split('|') + for col in range(len(fields)): + if col == 1: # Correct old entries made in Hertz + freq = fields[1] + try: + freq = float(freq) + except: + pass + else: + if freq > 30000.0: # Must be in Hertz + freq *= 1E-6 + fields[1] = self.FormatFloat(freq) + if col <= 5: + self.SetCellValue(row, col, fields[col].strip()) + self.MakeRepeaterDict() + def WriteOut(self): + ncols = self.GetNumberCols() + if ncols != 6: + print ("Bad logic in favorites WriteOut()") + return + self.changed = False + try: + fp = open(self.init_path, 'w') + except: + return + for row in range(self.GetNumberRows()): + out = [] + for col in range(0, ncols): + cell = self.GetCellValue(row, col) + cell = cell.replace('|', ';') + out.append(cell) + t = "%20s | %10s | %10s | %30s | %10s | %10s\n" % tuple(out) + fp.write(t) + fp.close() + def AddNewFavorite(self): + self.InsertRows(0) + self.SetCellValue(0, 0, 'New station'); + freq = (application.rxFreq + application.VFO) * 1E-6 # convert to megahertz + freq = self.FormatFloat(freq) + self.SetCellValue(0, 1, freq) + self.SetCellValue(0, 2, application.mode); + self.SetCellEditor(0, 2, wx.grid.GridCellChoiceEditor(self.mode_names, True)) + self.OnChange() + def OnRightClickLabel(self, event): + event.Skip() + self.menurow = event.GetRow() + if self.menurow >= 0: + pos = event.GetPosition() + self.PopupMenu(self.popupmenu, pos) + def OnLeftClickLabel(self, event): + self.OnRightClickLabel(event) + def OnLeftDClick(self, event): # Thanks to Christof, DJ4CM + self.menurow = event.GetRow() + if self.menurow >= 0: + self.OnPopupTuneto(event) + def OnPopupAppend(self, event): + self.InsertRows(self.menurow + 1) + self.SetCellEditor(self.menurow + 1, 2, wx.grid.GridCellChoiceEditor(self.mode_names, True)) + self.OnChange() + def OnPopupInsert(self, event): + self.InsertRows(self.menurow) + self.SetCellEditor(self.menurow, 2, wx.grid.GridCellChoiceEditor(self.mode_names, True)) + self.OnChange() + def OnPopupDelete(self, event): + self.DeleteRows(self.menurow) + if self.GetNumberRows() < 1: + self.AppendRows() + self.SetCellEditor(0, 2, wx.grid.GridCellChoiceEditor(self.mode_names, True)) + self.OnChange() + def OnPopupMoveUp(self, event): + row = self.menurow + if row < 1: + return + for i in range(self.GetNumberCols()): + c = self.GetCellValue(row - 1, i) + self.SetCellValue(row - 1, i, self.GetCellValue(row, i)) + self.SetCellValue(row, i, c) + def OnPopupMoveDown(self, event): + row = self.menurow + if row == self.GetNumberRows() - 1: + return + for i in range(self.GetNumberCols()): + c = self.GetCellValue(row + 1, i) + self.SetCellValue(row + 1, i, self.GetCellValue(row, i)) + self.SetCellValue(row, i, c) + def OnPopupTuneto(self, event): + freq = self.GetCellValue(self.menurow, 1) + if not freq: + return + try: + freq = str2freq (freq) + except ValueError: + print('Bad frequency') + return + if self.changed: + if self.timer.IsRunning(): + self.timer.Stop() + self.WriteOut() + application.ChangeRxTxFrequency(None, freq) + mode = self.GetCellValue(self.menurow, 2) + mode = mode.upper() + application.modeButns.SetLabel(mode, True) + application.screenBtnGroup.SetLabel(conf.default_screen, do_cmd=True) + def MakeRepeaterDict(self): + self.RepeaterDict = {} + for row in range(self.GetNumberRows()): + offset = self.GetCellValue(row, 4) + offset = offset.strip() + if not offset: + continue + freq = self.GetCellValue(row, 1) + tone = self.GetCellValue(row, 5) + tone = tone.strip() + if not tone: + tone = '0' + try: + offset = float(offset) + freq = float(freq) + tone = float(tone) + except: + traceback.print_exc() + else: + freq = int(freq * 1E6 + 0.5) # frequency in Hertz + freq = (freq + 500) // 1000 # frequency in units of 1 kHz + self.RepeaterDict[freq * 1000] = (offset, tone) + def OnChange(self, event=None): + self.MakeRepeaterDict() + self.changed = True + if self.timer.IsRunning(): + self.timer.Stop() + self.timer.Start(5000, oneShot=True) + def OnTimer(self, event): + if self.changed: + self.WriteOut() + +class GraphDisplay(wx.Window): + """Display the FFT graph within the graph screen.""" + def __init__(self, parent, x, y, graph_width, height, chary): + wx.Window.__init__(self, parent, + pos = (x, y), + size = (graph_width, height), + style = wx.NO_BORDER) + self.parent = parent + self.chary = chary + self.graph_width = graph_width + self.display_text = "" + self.line = [(0, 0), (1,1)] # initial fake graph data + self.SetBackgroundColour(conf.color_graph) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown) + self.Bind(wx.EVT_RIGHT_DOWN, parent.OnRightDown) + self.Bind(wx.EVT_LEFT_UP, parent.OnLeftUp) + self.Bind(wx.EVT_MOTION, parent.OnMotion) + self.Bind(wx.EVT_MOUSEWHEEL, parent.OnWheel) + self.tune_tx = graph_width // 2 # Current X position of the Tx tuning line + self.tune_rx = 0 # Current X position of Rx tuning line or zero + self.scale = 20 # pixels per 10 dB + self.peak_hold = 9999 # time constant for holding peak value + self.height = 10 + self.y_min = 1000 + self.y_max = 0 + self.max_height = application.screen_height + self.backgroundPen = wx.Pen(self.GetBackgroundColour(), 1) + self.tuningPenTx = wx.Pen(conf.color_txline, 1) + self.tuningPenRx = wx.Pen(conf.color_rxline, 1) + self.backgroundBrush = wx.Brush(self.GetBackgroundColour()) + self.filterBrush = wx.Brush(conf.color_bandwidth, wx.SOLID) + self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID) + self.font = wx.Font(conf.graph_msg_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL, + wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + self.SetFont(self.font) + if wxVersion in ('2', '3'): + self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM) + else: + self.SetBackgroundStyle(wx.BG_STYLE_PAINT) + def OnPaint(self, event): + #print 'GraphDisplay', self.GetUpdateRegion().GetBox() + dc = wx.AutoBufferedPaintDC(self) + dc.Clear() + # Draw the tuning line and filter display to the screen. + # If self.tune_rx is zero, draw the Rx filter at the Tx tuning line. There is no separate Rx display. + # Otherwise draw both an Rx and Tx tuning display. + self.DrawFilter(dc) + dc.SetPen(wx.Pen(conf.color_graphline, 1)) + dc.DrawLines(self.line) + dc.SetPen(self.horizPen) + for y in self.parent.y_ticks: + dc.DrawLine(0, y, self.graph_width, y) # y line + if self.display_text: + dc.SetFont(self.font) + dc.SetTextBackground(conf.color_graph_msg_bg) + dc.SetTextForeground(conf.color_graph_msg_fg) + dc.SetBackgroundMode(wx.SOLID) + dc.DrawText(self.display_text, 0, 0) + def DrawFilter(self, dc): + dc.SetPen(wx.TRANSPARENT_PEN) + dc.SetLogicalFunction(wx.COPY) + scale = 1.0 / self.parent.zoom / self.parent.sample_rate * self.graph_width + dc.SetBrush(self.filterBrush) + if self.tune_rx: + x, w, rit = self.parent.GetFilterDisplayXWR(rx_filters=False) + dc.DrawRectangle(self.tune_tx + x, 0, w, self.height) + x, w, rit = self.parent.GetFilterDisplayXWR(rx_filters=True) + dc.DrawRectangle(self.tune_rx + rit + x, 0, w, self.height) + dc.SetPen(self.tuningPenRx) + dc.DrawLine(self.tune_rx, 0, self.tune_rx, self.height) + else: + x, w, rit = self.parent.GetFilterDisplayXWR(rx_filters=True) + dc.DrawRectangle(self.tune_tx + rit + x, 0, w, self.height) + dc.SetPen(self.tuningPenTx) + dc.DrawLine(self.tune_tx, 0, self.tune_tx, self.height) + return rit + def SetHeight(self, height): + self.height = height + self.SetSize((self.graph_width, height)) + def OnGraphData(self, data): + x = 0 + for y in data: # y is in dB, -200 to 0 + y = self.zeroDB - int(y * self.scale / 10.0 + 0.5) + try: + y0 = self.line[x][1] + except IndexError: + self.line.append([x, y]) + else: + if y > y0: + y = min(y, y0 + self.peak_hold) + self.line[x] = [x, y] + x = x + 1 + self.Refresh() + def SetTuningLine(self, tune_tx, tune_rx): + dc = wx.ClientDC(self) + rit = self.parent.GetFilterDisplayRit() + # Erase the old display + dc.SetPen(self.backgroundPen) + if self.tune_rx: + dc.DrawLine(self.tune_rx, 0, self.tune_rx, self.height) + dc.DrawLine(self.tune_tx, 0, self.tune_tx, self.height) + # Draw a new display + if self.tune_rx: + dc.SetPen(self.tuningPenRx) + dc.DrawLine(tune_rx, 0, tune_rx, self.height) + dc.SetPen(self.tuningPenTx) + dc.DrawLine(tune_tx, 0, tune_tx, self.height) + self.tune_tx = tune_tx + self.tune_rx = tune_rx + +class GraphScreen(wx.Window): + """Display the graph screen X and Y axis, and create a graph display.""" + def __init__(self, parent, data_width, graph_width, in_splitter=0): + wx.Window.__init__(self, parent, pos = (0, 0)) + self.in_splitter = in_splitter # Are we in the top of a splitter window? + self.split_unavailable = False # Are we a multi receive graph or waterfall window? + if in_splitter: + self.y_scale = conf.waterfall_graph_y_scale + self.y_zero = conf.waterfall_graph_y_zero + else: + self.y_scale = conf.graph_y_scale + self.y_zero = conf.graph_y_zero + self.zoom_control = 0 + self.y_ticks = [] + self.VFO = 0 + self.filter_mode = 'AM' + self.filter_bandwidth = 0 + self.filter_center = 0 + self.ritFreq = 0 # receive incremental tuning frequency offset + self.mouse_x = 0 + self.WheelMod = conf.mouse_wheelmod # Round frequency when using mouse wheel + self.txFreq = 0 + self.sample_rate = application.sample_rate + self.zoom = 1.0 + self.zoom_deltaf = 0 + self.data_width = data_width + self.graph_width = graph_width + self.doResize = False + self.pen_tick = wx.Pen(conf.color_graphticks, 1) + self.pen_center = wx.Pen(conf.color_graphticks, 3) + self.font = wx.Font(conf.graph_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL, + wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + self.SetFont(self.font) + w = self.GetCharWidth() * 14 // 10 + h = self.GetCharHeight() + self.charx = w + self.chary = h + self.tick = max(2, h * 3 // 10) + self.originX = w * 5 + self.offsetY = h + self.tick + self.width = self.originX + self.graph_width + self.tick + self.charx * 2 + self.height = application.screen_height * 3 // 10 + self.x0 = self.originX + self.graph_width // 2 # center of graph + self.tuningX = self.x0 + self.originY = 10 + self.zeroDB = 10 # y location of zero dB; may be above the top of the graph + self.scale = 10 + self.mouse_is_rx = False + self.SetSize((self.width, self.height)) + self.SetSizeHints(self.width, 1, self.width) + self.SetBackgroundColour(conf.color_graph) + self.backgroundBrush = wx.Brush(conf.color_graph) + self.Bind(wx.EVT_SIZE, self.OnSize) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) + self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) + self.Bind(wx.EVT_MOTION, self.OnMotion) + self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) + self.MakeDisplay() + def MakeDisplay(self): + self.display = GraphDisplay(self, self.originX, 0, self.graph_width, 5, self.chary) + self.display.zeroDB = self.zeroDB + def SetDisplayMsg(self, text=''): + self.display.display_text = text + self.display.Refresh() + def ScrollMsg(self, chars): # Add characters to a scrolling message + self.display.display_text = self.display.display_text + chars + self.display.display_text = self.display.display_text[-50:] + self.display.Refresh() + def OnPaint(self, event): + dc = wx.PaintDC(self) + dc.SetBackground(self.backgroundBrush) + dc.Clear() + dc.SetFont(self.font) + dc.SetTextForeground(conf.color_graphlabels) + if self.in_splitter: + self.MakeYTicks(dc) + else: + self.MakeYTicks(dc) + self.MakeXTicks(dc) + def OnIdle(self, event): + if self.doResize: + self.ResizeGraph() + def OnSize(self, event): + self.doResize = True + event.Skip() + def ResizeGraph(self): + """Change the height of the graph. + + Changing the width interactively is not allowed because the FFT size is fixed. + Call after changing the zero or scale to recalculate the X and Y axis marks. + """ + w, h = self.GetClientSize() + if self.in_splitter: # Splitter window has no X axis scale + self.height = h + self.originY = h + else: + self.height = h - self.chary # Leave space for X scale + self.originY = self.height - self.offsetY + if self.originY < 0: + self.originY = 0 + self.MakeYScale() + self.display.SetHeight(self.originY) + self.display.scale = self.scale + self.doResize = False + self.Refresh() + def ChangeYscale(self, y_scale): + self.y_scale = y_scale + self.doResize = True + def ChangeYzero(self, y_zero): + self.y_zero = y_zero + self.doResize = True + def ChangeZoom(self, zoom, deltaf, zoom_control): + self.zoom = zoom + self.zoom_deltaf = deltaf + self.zoom_control = zoom_control + self.doResize = True + def MakeYScale(self): + chary = self.chary + scale = (self.originY - chary) * 10 // (self.y_scale + 20) # Number of pixels per 10 dB + scale = max(1, scale) + q = (self.originY - chary ) // scale // 2 + zeroDB = chary + q * scale - self.y_zero * scale // 10 + if zeroDB > chary: + zeroDB = chary + self.scale = scale + self.zeroDB = zeroDB + self.display.zeroDB = self.zeroDB + QS.record_graph(self.originX, self.zeroDB, self.scale) + def MakeYTicks(self, dc): + chary = self.chary + x1 = self.originX - self.tick * 3 # left of tick mark + x2 = self.originX - 1 # x location of y axis + x3 = self.originX + self.graph_width # end of graph data + dc.SetPen(self.pen_tick) + dc.DrawLine(x2, 0, x2, self.originY + 1) # y axis + y = self.zeroDB + del self.y_ticks[:] + y_old = y + for i in range(0, -99999, -10): + if y >= chary // 2: + dc.SetPen(self.pen_tick) + dc.DrawLine(x1, y, x2, y) # y tick + self.y_ticks.append(y) + t = repr(i) + w, h = dc.GetTextExtent(t) + # draw text on Y axis + if y - y_old > h: + if y + h // 2 <= self.originY: + dc.DrawText(repr(i), x1 - w, y - h // 2) + elif h < self.scale: + dc.DrawText(repr(i), x1 - w, self.originY - h) + y_old = y + y = y + self.scale + if y >= self.originY - 3: + break + def MakeXTicks(self, dc): + sample_rate = int(self.sample_rate * self.zoom) + VFO = self.VFO + self.zoom_deltaf + originY = self.originY + x3 = self.originX + self.graph_width # end of fft data + charx , z = dc.GetTextExtent('-30000XX') + tick0 = self.tick + tick1 = tick0 * 2 + tick2 = tick0 * 3 + # Draw the X axis + dc.SetPen(self.pen_tick) + dc.DrawLine(self.originX, originY, x3, originY) + # Draw the band plan colors below the X axis + x = self.originX + f = float(x - self.x0) * sample_rate / self.data_width + c = None + y = originY + 1 + for freq, color in application.BandPlan: + freq -= VFO + if f < freq: + xend = int(self.x0 + float(freq) * self.data_width / sample_rate + 0.5) + if c is not None: + dc.SetPen(wx.TRANSPARENT_PEN) + dc.SetBrush(wx.Brush(c)) + dc.DrawRectangle(x, y, min(x3, xend) - x, tick0) # x axis + if xend >= x3: + break + x = xend + f = freq + c = color + # check the width of the frequency label versus frequency span + df = charx * sample_rate // self.data_width + if VFO >= 10E9: # Leave room for big labels + df *= 1.33 + elif VFO >= 1E9: + df *= 1.17 + # tfreq: tick frequency for labels in Hertz + # stick: small tick in Hertz + # mtick: medium tick + # ltick: large tick + s2 = 1000 + tfreq = None + while tfreq is None: + if df < s2: + tfreq = s2 + stick = s2 // 10 + mtick = s2 // 2 + ltick = tfreq + elif df < s2 * 2: + tfreq = s2 * 2 + stick = s2 // 10 + mtick = s2 // 2 + ltick = s2 + elif df < s2 * 5: + tfreq = s2 * 5 + stick = s2 // 2 + mtick = s2 + ltick = tfreq + s2 *= 10 + # Draw the X axis ticks and frequency in kHz + dc.SetPen(self.pen_tick) + freq1 = VFO - sample_rate // 2 + freq1 = (freq1 // stick) * stick + freq2 = freq1 + sample_rate + stick + 1 + y_end = 0 + for f in range (freq1, freq2, stick): + x = self.x0 + int(float(f - VFO) / sample_rate * self.data_width) + if self.originX <= x <= x3: + if f % ltick == 0: # large tick + dc.DrawLine(x, originY, x, originY + tick2) + elif f % mtick == 0: # medium tick + dc.DrawLine(x, originY, x, originY + tick1) + else: # small tick + dc.DrawLine(x, originY, x, originY + tick0) + if f % tfreq == 0: # place frequency label + t = str(f//1000) + w, h = dc.GetTextExtent(t) + dc.DrawText(t, x - w // 2, originY + tick2) + y_end = originY + tick2 + h + if y_end: # mark the center of the display + dc.SetPen(self.pen_center) + dc.DrawLine(self.x0, y_end, self.x0, application.screen_height) + def OnGraphData(self, data): + i1 = (self.data_width - self.graph_width) // 2 + i2 = i1 + self.graph_width + self.raw_graph_data = data[i1:i2] + self.display.OnGraphData(data[i1:i2]) + def SetVFO(self, vfo): + self.VFO = vfo + self.doResize = True + def SetTxFreq(self, tx_freq, rx_freq): + sample_rate = int(self.sample_rate * self.zoom) + self.txFreq = tx_freq + tx_x = self.x0 + int(float(tx_freq - self.zoom_deltaf) / sample_rate * self.data_width) + self.tuningX = tx_x + rx_x = self.x0 + int(float(rx_freq - self.zoom_deltaf) / sample_rate * self.data_width) + if abs(tx_x - rx_x) < 2: # Do not display Rx line for small frequency offset + self.display.SetTuningLine(tx_x - self.originX, 0) + else: + self.display.SetTuningLine(tx_x - self.originX, rx_x - self.originX) + def GetFilterDisplayXWR(self, rx_filters): + mode = self.filter_mode + rit = self.ritFreq + if rx_filters: # return Rx filter + bandwidth = self.filter_bandwidth + center = self.filter_center + else: # return Tx filter + bandwidth, center = get_filter_tx(mode) + x = center - bandwidth // 2 + scale = 1.0 / self.zoom / self.sample_rate * self.data_width + x = int(x * scale + 0.5) + bandwidth = int(bandwidth * scale + 0.5) + if bandwidth < 2: + bandwidth = 1 + rit = int(rit * scale + 0.5) + return x, bandwidth, rit # Starting x, bandwidth and RIT frequency + def GetFilterDisplayRit(self): + rit = self.ritFreq + scale = 1.0 / self.zoom / self.sample_rate * self.data_width + rit = int(rit * scale + 0.5) + return rit + def GetMousePosition(self, event): + """For mouse clicks in our display, translate to our screen coordinates.""" + mouse_x, mouse_y = event.GetPosition() + win = event.GetEventObject() + if win is not self: + x, y = win.GetPosition().Get() + mouse_x += x + mouse_y += y + return mouse_x, mouse_y + def FreqRound(self, tune, vfo): + if conf.freq_spacing and not conf.freq_round_ssb: + freq = tune + vfo + n = int(freq) - conf.freq_base + if n >= 0: + n = (n + conf.freq_spacing // 2) // conf.freq_spacing + else: + n = - ( - n + conf.freq_spacing // 2) // conf.freq_spacing + freq = conf.freq_base + n * conf.freq_spacing + return freq - vfo + else: + return tune + def OnRightDown(self, event): + sample_rate = int(self.sample_rate * self.zoom) + VFO = self.VFO + self.zoom_deltaf + mouse_x, mouse_y = self.GetMousePosition(event) + freq = float(mouse_x - self.x0) * sample_rate / self.data_width + freq = int(freq) + if VFO > 0: + vfo = VFO + freq - self.zoom_deltaf + if sample_rate > 40000: + vfo = (vfo + 5000) // 10000 * 10000 # round to even number + elif sample_rate > 5000: + vfo = (vfo + 500) // 1000 * 1000 + else: + vfo = (vfo + 50) // 100 * 100 + tune = freq + VFO - vfo + tune = self.FreqRound(tune, vfo) + self.ChangeHwFrequency(tune, vfo, 'MouseBtn3', event=event) + def OnLeftDown(self, event): + sample_rate = int(self.sample_rate * self.zoom) + mouse_x, mouse_y = self.GetMousePosition(event) + if mouse_x <= self.originX: # click left of Y axis + return + if mouse_x >= self.originX + self.graph_width: # click past FFT data + return + shift = wx.GetKeyState(wx.WXK_SHIFT) + if shift: + mouse_x -= self.filter_center * self.data_width / sample_rate + self.mouse_x = mouse_x + x = mouse_x - self.originX + if self.split_unavailable: + self.mouse_is_rx = False + elif application.split_rxtx and application.split_locktx: + self.mouse_is_rx = True + elif application.split_rxtx and application.split_lockrx: + self.mouse_is_rx = False + elif self.display.tune_rx and abs(x - self.display.tune_tx) > abs(x - self.display.tune_rx): + self.mouse_is_rx = True + else: + self.mouse_is_rx = False + if mouse_y < self.originY: # click above X axis + freq = float(mouse_x - self.x0) * sample_rate / self.data_width + self.zoom_deltaf + freq = int(freq) + if shift: + pass + elif conf.freq_spacing: + if not self.mouse_is_rx: + freq = self.FreqRound(freq, self.VFO) + elif application.mode in ('LSB', 'USB', 'AM', 'FM', 'FDV-U', 'FDV-L'): + rnd = conf.freq_round_ssb + if rnd: + freq = (freq + rnd // 2) // rnd * rnd + elif application.mode in ('CWU', 'CWL'): # Move to a nearby peak + CW_width = self.data_width * application.filter_bandwidth // sample_rate // 2 # width tolerance + CW_mouse = mouse_x - self.originX + CW_max = -9999 # look for a peak significantly greater than the average + CW_avg = 0.0 + CW_x1 = CW_mouse - CW_width + CW_x2 = CW_mouse + CW_width + try: + for x in range(CW_x1, CW_x2): + y = self.raw_graph_data[x] + CW_avg += y + if y > CW_max: + CW_max = y + CW_x = x + CW_avg /= (CW_x2 - CW_x1) + yp = self.raw_graph_data[CW_x + 1] + ym = self.raw_graph_data[CW_x - 1] + CW_correct = 0.5 * (ym - yp) / (ym - 2 * CW_max + yp) + except: # missing or short raw_graph_data, zero CW_width + pass + else: + if CW_max - CW_avg > 5 and CW_max > yp and CW_max > ym: + CW_x += CW_correct + freq = float(CW_x + self.originX - self.x0) * sample_rate / self.data_width + self.zoom_deltaf + freq = int(freq) + if self.mouse_is_rx: + self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseBtn1', event=event, rx_freq=freq) + else: + self.ChangeHwFrequency(freq, self.VFO, 'MouseBtn1', event=event) + self.CaptureMouse() + def OnLeftUp(self, event): + if self.HasCapture(): + self.ReleaseMouse() + freq = self.FreqRound(self.txFreq, self.VFO) + if freq != self.txFreq: + self.ChangeHwFrequency(freq, self.VFO, 'MouseMotion', event=event) + def OnMotion(self, event): + sample_rate = int(self.sample_rate * self.zoom) + if event.Dragging() and event.LeftIsDown(): + mouse_x, mouse_y = self.GetMousePosition(event) + if wx.GetKeyState(wx.WXK_SHIFT): + mouse_x -= self.filter_center * self.data_width / sample_rate + if conf.mouse_tune_method: # Mouse motion changes the VFO frequency + x = (mouse_x - self.mouse_x) # Thanks to VK6JBL + self.mouse_x = mouse_x + freq = float(x) * sample_rate / self.data_width + freq = int(freq) + self.ChangeHwFrequency(self.txFreq, self.VFO - freq, 'MouseMotion', event=event) + else: # Mouse motion changes the tuning frequency + # Frequency changes more rapidly for higher mouse Y position + speed = max(10, self.originY - mouse_y) / float(self.originY + 1) + x = (mouse_x - self.mouse_x) + self.mouse_x = mouse_x + freq = speed * x * sample_rate / self.data_width + freq = int(freq) + if self.mouse_is_rx: # Mouse motion changes the receive frequency + freq2 = application.rxFreq + freq + self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseMotion', event=event, rx_freq=freq2) + else: # Mouse motion changes the transmit frequency + self.ChangeHwFrequency(self.txFreq + freq, self.VFO, 'MouseMotion', event=event) + def OnWheel(self, event): + if conf.freq_spacing: + wm = conf.freq_spacing + else: + wm = self.WheelMod # Round frequency when using mouse wheel + mouse_x, mouse_y = self.GetMousePosition(event) + x = mouse_x - self.originX + if self.split_unavailable: + self.mouse_is_rx = False + elif application.split_rxtx and application.split_locktx: + self.mouse_is_rx = True + elif application.split_rxtx and application.split_lockrx: + self.mouse_is_rx = False + elif self.display.tune_rx and abs(x - self.display.tune_tx) > abs(x - self.display.tune_rx): + self.mouse_is_rx = True + else: + self.mouse_is_rx = False + if self.mouse_is_rx: + freq = application.rxFreq + self.VFO + wm * event.GetWheelRotation() // event.GetWheelDelta() + if conf.freq_spacing: + freq = self.FreqRound(freq, 0) + elif freq >= 0: + freq = freq // wm * wm + else: # freq can be negative when the VFO is zero + freq = - (- freq // wm * wm) + tune = freq - self.VFO + self.ChangeHwFrequency(self.txFreq, self.VFO, 'MouseWheel', event=event, rx_freq=tune) + else: + freq = self.txFreq + self.VFO + wm * event.GetWheelRotation() // event.GetWheelDelta() + if conf.freq_spacing: + freq = self.FreqRound(freq, 0) + elif freq >= 0: + freq = freq // wm * wm + else: # freq can be negative when the VFO is zero + freq = - (- freq // wm * wm) + tune = freq - self.VFO + self.ChangeHwFrequency(tune, self.VFO, 'MouseWheel', event=event) + def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None, rx_freq=None): + application.ChangeHwFrequency(tune, vfo, source, band, event, rx_freq) + def PeakHold(self, name): + if name in ('GraphP1', 'WFallP1'): + self.display.peak_hold = int(self.display.scale * conf.graph_peak_hold_1) + elif name in ('GraphP2', 'WFallP2'): + self.display.peak_hold = int(self.display.scale * conf.graph_peak_hold_2) + else: + self.display.peak_hold = 9999 + if self.display.peak_hold < 1: + self.display.peak_hold = 1 + +class StationScreen(wx.Window): # This code was contributed by Christof, DJ4CM. Many Thanks!! Edited May 2022. + """Create a window below the graph X axis to display interesting frequencies.""" + def __init__(self, parent, width, lines): + self.lineMargin = 2 + self.lines = lines + self.mouse_x = 0 + self.stationList = [] + graph = self.graph = application.graph + height = lines * (graph.GetCharHeight() + self.lineMargin) # The height may be zero + wx.Window.__init__(self, parent, size=(graph.width, height), style = wx.NO_BORDER) + self.font = wx.Font(conf.graph_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL, + wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + self.SetFont(self.font) + self.SetBackgroundColour(conf.color_graph) + self.width = application.screen_width + self.Bind(wx.EVT_PAINT, self.OnPaint) + if lines: + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_MOTION, self.OnMotion) + self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow) + # handle station info + self.stationWindow = wx.PopupWindow (parent) + self.stationInfo = wx.TextCtrl(self.stationWindow, style=wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_NO_VSCROLL) + self.stationInfo.SetFont(wx.Font(conf.status_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL, + wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface)) + self.stationWindow.Hide(); + self.firstStationInRange = None + self.lastStationX = 0 + self.nrStationInRange = 0 + self.tunedStation = 0 + def OnPaint(self, event): + dc = wx.PaintDC(self) + if not self.lines: + return + dc.SetFont(self.font) + graph = self.graph + dc.SetTextForeground(conf.color_graphlabels) + dc.SetPen(graph.pen_tick) + originX = graph.originX + originY = graph.originY + endX = originX + graph.graph_width # end of fft data + sample_rate = int(graph.sample_rate * graph.zoom) + VFO = graph.VFO + graph.zoom_deltaf + hl = self.GetCharHeight() + y = 0 + for i in range (self.lines): + dc.DrawLine(originX, y, endX, y) + y += hl + self.lineMargin + # create a sorted list of favorites in the frequency range + freq1 = VFO - sample_rate // 2 + freq2 = VFO + sample_rate // 2 + self.stationList = [] + fav = application.config_screen.favorites + for row in range (fav.GetNumberRows()): + fav_f = fav.GetCellValue(row, 1) + if fav_f: + try: + fav_f = str2freq(fav_f) + if freq1 < fav_f < freq2: + self.stationList.append((fav_f, conf.Xsym_stat_fav, fav.GetCellValue(row, 0), + fav.GetCellValue(row, 2), fav.GetCellValue(row, 3))) + except ValueError: + pass + # add memory stations + for mem_f, mem_band, mem_vfo, mem_txfreq, mem_mode in application.memoryState: + if freq1 < mem_f < freq2: + self.stationList.append((mem_f, conf.Xsym_stat_mem, '', mem_mode, '')) + #add dx spots + if application.dxCluster: + for entry in application.dxCluster.dxSpots: + if freq1 < entry.getFreq() < freq2: + for i in range (0, entry.getLen()): + descr = entry.getSpotter(i) + '\t' + entry.getTime(i) + '\t' + entry.getLocation(i) + '\n' + entry.getComment(i) + if i < entry.getLen()-1: + descr += '\n' + self.stationList.append((entry.freq, conf.Xsym_stat_dx, entry.dx, '', descr)) + # draw stations on graph + self.stationList.sort() + lastX = [] + line = 0 + for i in range (0, self.lines): + lastX.append(graph.width) + for statFreq, symbol, statName, statMode, statDscr in reversed (self.stationList): + ws = dc.GetTextExtent(symbol)[0] + statX = graph.x0 + int(float(statFreq - VFO) / sample_rate * graph.data_width) + w, h = dc.GetTextExtent(statName) + # shorten name until it fits into remaining space + maxLen = 25 + tName = statName + while (w > lastX[line] - statX - ws - 4) and maxLen > 0: + maxLen -= 1 + tName = statName[:maxLen] + '..' + w, h = dc.GetTextExtent(tName) + dc.DrawLine(statX, line * (hl+self.lineMargin), statX, line * (hl+self.lineMargin) + 4) + dc.DrawText(symbol + ' ' + tName, statX - ws//2, line * (hl+self.lineMargin) + self.lineMargin//2+1) + lastX[line] = statX + line = (line+1)%self.lines + def OnLeftDown(self, event): + if self.firstStationInRange != None: + # tune to station + if self.tunedStation >= self.nrStationInRange: + self.tunedStation = 0 + freq, symbol, name, mode, dscr = self.stationList[self.firstStationInRange+self.tunedStation] + self.tunedStation += 1 + if mode != '': # information about mode available + mode = mode.upper() + application.modeButns.SetLabel(mode, True) + application.ChangeRxTxFrequency(None, freq) + def OnMotion(self, event): + mouse_x, mouse_y = event.GetPosition() + x = (mouse_x - self.mouse_x) + application.isTuning = False + # show detailed station info + if abs(self.lastStationX - mouse_x) > 30: + self.firstStationInRange = None + found = False + graph = self.graph + sample_rate = int(graph.sample_rate * graph.zoom) + VFO = graph.VFO + graph.zoom_deltaf + if abs(x) > 5: # ignore small mouse moves + for index in range (0, len(self.stationList)): + statFreq, symbol, statName, statMode, statDscr = self.stationList[index] + statX = graph.x0 + int(float(statFreq - VFO) / sample_rate * graph.data_width) + if abs(mouse_x-statX) < 10: + self.lastStationX = mouse_x + if found == False: + self.firstStationInRange = index + self.firstStationX = statX + self.nrStationInRange = 0 + self.stationInfo.Clear() + found = True + self.nrStationInRange += 1 + t = " %s %s" % (symbol, statName) + width, height = self.stationInfo.GetTextExtent(t) + txt = t + t = " %s Hz %s" % (FreqFormatter(statFreq), statMode) + w, h = self.stationInfo.GetTextExtent(t) + width = max(width, w) + height += h + txt += '\n' + txt += t + if len(statDscr) > 0: + t = " %s" % statDscr + w, h = self.stationInfo.GetTextExtent(t) + width = max(width, w) + height += h + txt += '\n' + txt += t + self.stationInfo.write(txt) + width += self.stationInfo.GetCharWidth() * 3 + height += self.stationInfo.GetCharHeight() // 2 + self.stationInfo.SetSize(wx.Size(width, height)) + self.stationWindow.SetClientSize((width, height)) + self.mouse_x = mouse_x + if self.firstStationInRange != None: + w, h = self.stationInfo.GetSize() + # convert coordinates to screen + rect = self.GetScreenRect() + self.stationWindow.Move(self.firstStationX, rect.GetY() - h) + if not self.stationWindow.IsShown(): + self.stationWindow.Show() + else: + self.stationWindow.Hide() + def OnLeaveWindow(self, event): + self.stationWindow.Hide() + + +class WaterfallDisplay(wx.Window): + """Create a waterfall display within the waterfall screen.""" + def __init__(self, parent, x, y, graph_width, height, margin): + wx.Window.__init__(self, parent, + pos = (x, y), + size = (graph_width, height), + style = wx.NO_BORDER) + self.parent = parent + self.graph_width = graph_width + self.margin = margin + self.height = 10 + self.zoom = 1.0 + self.zoom_deltaf = 0 + self.rf_gain = 0 # Keep waterfall colors constant for variable RF gain + self.sample_rate = application.sample_rate + self.SetBackgroundColour('Black') + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown) + self.Bind(wx.EVT_RIGHT_DOWN, parent.OnRightDown) + self.Bind(wx.EVT_LEFT_UP, parent.OnLeftUp) + self.Bind(wx.EVT_MOTION, parent.OnMotion) + self.Bind(wx.EVT_MOUSEWHEEL, parent.OnWheel) + self.tune_tx = graph_width // 2 # Current X position of the Tx tuning line + self.tune_rx = 0 # Current X position of Rx tuning line or zero + self.marginPen = wx.Pen(conf.color_graph, 1) + self.tuningPen = wx.Pen('White', 3) + self.tuningPenTx = wx.Pen(conf.color_txline, 3) + self.tuningPenRx = wx.Pen(conf.color_rxline, 3) + self.filterBrush = wx.Brush(conf.color_bandwidth, wx.SOLID) + #self.backgroundBrush = wx.Brush(conf.color_graph) + # Size of top faster scroll region is (top_key + 2) * (top_key - 1) // 2 + self.top_key = 8 + self.top_size = (self.top_key + 2) * (self.top_key - 1) // 2 + # Make the palette + if conf.waterfall_palette == 'B': + pal2 = conf.waterfallPaletteB + elif conf.waterfall_palette == 'C': + pal2 = conf.waterfallPaletteC + else: + pal2 = conf.waterfallPalette + red = bytearray(256) + green = bytearray(256) + blue = bytearray(256) + n = 0 + for i in range(256): + if i > pal2[n+1][0]: + n = n + 1 + red[i] = (i - pal2[n][0]) * (pal2[n+1][1] - pal2[n][1]) // (pal2[n+1][0] - pal2[n][0]) + pal2[n][1] + green[i] = (i - pal2[n][0]) * (pal2[n+1][2] - pal2[n][2]) // (pal2[n+1][0] - pal2[n][0]) + pal2[n][2] + blue[i] = (i - pal2[n][0]) * (pal2[n+1][3] - pal2[n][3]) // (pal2[n+1][0] - pal2[n][0]) + pal2[n][3] + self.rgb_data = QS.watfall_RgbData(red, green, blue, self.graph_width, application.screen_height) + self.pixels = bytearray(self.graph_width * application.screen_height * 3) + def OnPaint(self, event): + dc = wx.PaintDC(self) + dc.SetTextForeground(conf.color_graphlabels) + dc.SetBackground(wx.Brush('Black')) + rit = self.DrawFilter(dc) + dc.SetLogicalFunction(wx.COPY) + sample_rate = int(self.sample_rate * self.zoom) + x_origin = int(float(self.VFO) / sample_rate * self.data_width + 0.5) + width = self.graph_width + height = self.height - self.margin + if height <= 0: + height = 1 + QS.watfall_GetPixels(self.rgb_data, self.pixels, x_origin, width, height) + if wxVersion in ('2', '3'): + bmap = wx.BitmapFromBuffer(width, height, self.pixels) + else: + bmap = wx.Bitmap.FromBuffer(width, height, self.pixels) + dc.DrawBitmap(bmap, 0, self.margin) + dc.SetPen(self.tuningPen) + dc.SetLogicalFunction(wx.XOR) + dc.DrawLine(self.tune_tx, self.margin, self.tune_tx, self.height) + if self.tune_rx: + dc.DrawLine(self.tune_rx, self.margin, self.tune_rx, self.height) + def SetHeight(self, height): + self.height = height + self.SetSize((self.graph_width, height)) + def DrawFilter(self, dc): + # Erase area at the top of the waterfall + dc.SetPen(wx.TRANSPARENT_PEN) + dc.SetLogicalFunction(wx.COPY) + dc.SetBrush(self.parent.backgroundBrush) + dc.DrawRectangle(0, 0, self.graph_width, self.margin) + # Draw the filter and top tuning lines + scale = 1.0 / self.zoom / self.sample_rate * self.data_width + dc.SetBrush(self.filterBrush) + if self.tune_rx: + x, w, rit = self.parent.GetFilterDisplayXWR(rx_filters=False) + dc.DrawRectangle(self.tune_tx + x, 0, w, self.margin) + x, w, rit = self.parent.GetFilterDisplayXWR(rx_filters=True) + dc.DrawRectangle(self.tune_rx + rit + x, 0, w, self.margin) + dc.SetPen(self.tuningPenRx) + dc.DrawLine(self.tune_rx, 0, self.tune_rx, self.margin) + else: + x, w, rit = self.parent.GetFilterDisplayXWR(rx_filters=True) + dc.DrawRectangle(self.tune_tx + rit + x, 0, w, self.margin) + dc.SetPen(self.tuningPenTx) + dc.DrawLine(self.tune_tx, 0, self.tune_tx, self.margin) + return rit + def OnGraphData(self, data, y_zero, y_scale): + # y_scale and y_zero range from zero to 160. + # y_zero controls the center position of the colors. Set to a bit over the noise level. + # y_scale controls how much the colors change when the sample deviates from y_zero. + gain = self.rf_gain + sample_rate = int(self.sample_rate * self.zoom) + x_origin = int(float(self.VFO) / sample_rate * self.data_width + 0.5) + QS.watfall_OnGraphData(self.rgb_data, data, y_zero, y_scale, gain, x_origin) + self.Refresh(False) + def SetTuningLine(self, tune_tx, tune_rx): + dc = wx.ClientDC(self) + rit = self.DrawFilter(dc) + dc.SetPen(self.tuningPen) + dc.SetLogicalFunction(wx.XOR) + dc.DrawLine(self.tune_tx, self.margin, self.tune_tx, self.height) + if self.tune_rx: + dc.DrawLine(self.tune_rx, self.margin, self.tune_rx, self.height) + dc.DrawLine(tune_rx, self.margin, tune_rx, self.height) + dc.DrawLine(tune_tx, self.margin, tune_tx, self.height) + self.tune_tx = tune_tx + self.tune_rx = tune_rx + def ChangeZoom(self, zoom, deltaf, zoom_control): + self.zoom = zoom + self.zoom_deltaf = deltaf + self.zoom_control = zoom_control + +class WaterfallScreen(wx.SplitterWindow): + """Create a splitter window with a graph screen and a waterfall screen""" + def __init__(self, frame, width, data_width, graph_width): + self.y_scale = conf.waterfall_y_scale + self.y_zero = conf.waterfall_y_zero + self.zoom_control = 0 + wx.SplitterWindow.__init__(self, frame) + self.SetSizeHints(width, -1, width) + self.SetSashGravity(0.50) + self.SetMinimumPaneSize(1) + self.SetSize((width, conf.waterfall_graph_size + 100)) # be able to set sash size + self.pane1 = GraphScreen(self, data_width, graph_width, 1) + self.pane2 = WaterfallPane(self, data_width, graph_width) + self.SplitHorizontally(self.pane1, self.pane2, conf.waterfall_graph_size) + def SetDisplayMsg(self, text=''): + self.pane1.SetDisplayMsg(text) + def ScrollMsg(self, char): # Add a character to a scrolling message + self.pane1.ScrollMsg(char) + def OnIdle(self, event): + self.pane1.OnIdle(event) + self.pane2.OnIdle(event) + def SetTxFreq(self, tx_freq, rx_freq): + self.pane1.SetTxFreq(tx_freq, rx_freq) + self.pane2.SetTxFreq(tx_freq, rx_freq) + def SetVFO(self, vfo): + self.pane1.SetVFO(vfo) + self.pane2.SetVFO(vfo) + def ChangeYscale(self, y_scale): # Test if the shift key is down + if wx.GetKeyState(wx.WXK_SHIFT): # Set graph screen + self.pane1.ChangeYscale(y_scale) + else: # Set waterfall screen + self.y_scale = y_scale + self.pane2.ChangeYscale(y_scale) + def ChangeYzero(self, y_zero): # Test if the shift key is down + if wx.GetKeyState(wx.WXK_SHIFT): # Set graph screen + self.pane1.ChangeYzero(y_zero) + else: # Set waterfall screen + self.y_zero = y_zero + self.pane2.ChangeYzero(y_zero) + def SetPane1(self, ysz): + y_scale, y_zero = ysz + self.pane1.ChangeYscale(y_scale) + self.pane1.ChangeYzero(y_zero) + def SetPane2(self, ysz): + y_scale, y_zero = ysz + self.y_scale = y_scale + self.pane2.ChangeYscale(y_scale) + self.y_zero = y_zero + self.pane2.ChangeYzero(y_zero) + def OnGraphData(self, data): + self.pane1.OnGraphData(data) + self.pane2.OnGraphData(data) + def ChangeRfGain(self, gain): # Set the correction for RF gain + self.pane2.display.rf_gain = gain + def ChangeZoom(self, zoom, deltaf, zoom_control): + self.zoom_control = zoom_control + self.pane1.ChangeZoom(zoom, deltaf, zoom_control) + self.pane2.ChangeZoom(zoom, deltaf, zoom_control) + self.pane2.display.ChangeZoom(zoom, deltaf, zoom_control) + def PeakHold(self, name): + self.pane1.PeakHold(name) + +class WaterfallPane(GraphScreen): + """Create a waterfall screen with an X axis and a waterfall display.""" + def __init__(self, frame, data_width, graph_width): + GraphScreen.__init__(self, frame, data_width, graph_width) + self.y_scale = conf.waterfall_y_scale + self.y_zero = conf.waterfall_y_zero + self.zoom_control = 0 + self.oldVFO = self.VFO + self.filter_mode = 'AM' + self.filter_bandwidth = 0 + self.filter_center = 0 + self.ritFreq = 0 # receive incremental tuning frequency offset + def MakeDisplay(self): + self.display = WaterfallDisplay(self, self.originX, 0, self.graph_width, 5, self.chary) + self.display.VFO = self.VFO + self.display.data_width = self.data_width + def SetVFO(self, vfo): + GraphScreen.SetVFO(self, vfo) + self.display.VFO = vfo + if self.oldVFO != vfo: + self.oldVFO = vfo + self.Refresh() + def MakeYTicks(self, dc): + pass + def ChangeYscale(self, y_scale): + self.y_scale = y_scale + def ChangeYzero(self, y_zero): + self.y_zero = y_zero + def OnGraphData(self, data): + i1 = (self.data_width - self.graph_width) // 2 + i2 = i1 + self.graph_width + self.raw_graph_data = data[i1:i2] + self.display.OnGraphData(data[i1:i2], self.y_zero, self.y_scale) + +class MultiRxGraph(GraphScreen): + # The screen showing each added receiver + the_modes = ('CWL', 'CWU', 'LSB', 'USB', 'AM', 'FM', 'DGT-U', 'DGT-L', 'DGT-FM', 'DGT-IQ') + def __init__(self, parent, data_width, graph_width, index): + multi_rx = application.multi_rx_screen + width = multi_rx.rx_data_width + GraphScreen.__init__(self, parent, width, width) + self.graph_display = self.display + self.waterfall_display = WaterfallDisplay(self, self.originX, 0, self.graph_width, 5, self.chary) + self.waterfall_display.Hide() + self.waterfall_display.VFO = self.VFO + self.waterfall_display.data_width = self.data_width + self.waterfall_y_scale = conf.waterfall_y_scale + self.waterfall_y_zero = conf.waterfall_y_zero + self.split_unavailable = True + width = self.originX + self.graph_width + self.tabX = width + (multi_rx.graph.width - width - multi_rx.rx_btn_width) // 2 + self.popupX = self.tabX - multi_rx.rx_btn_width * 2 + self.multirx_index = index + self.is_playing = False + self.mode_index = 0 + self.band = '40' + # Create controls + posY = 0 + half_width = multi_rx.rx_btn_width // 2 + half_size = half_width, multi_rx.rx_btn_height + self.rx_btn = QuiskPushbutton(self, self.OnPopButton, "Rx %d .." % (index + 1)) + self.rx_btn.SetSize(half_size) + self.rx_btn.SetPosition((self.tabX, posY)) + self.play_btn = QuiskCheckbutton(self, self.OnPlayButton, "Play") + self.play_btn.SetSize(half_size) + self.play_btn.SetPosition((self.tabX + half_width, posY)) + posY += multi_rx.rx_btn_height + btn1 = QuiskPushbutton(self, self.OnBtnDownBand, conf.Xbtn_text_range_dn, use_right=True) + btn2 = QuiskPushbutton(self, self.OnBtnUpBand, conf.Xbtn_text_range_up, use_right=True) + btn1.SetSize(half_size) + btn2.SetSize(half_size) + btn1.SetPosition((self.tabX, posY)) + btn2.SetPosition((self.tabX + half_width, posY)) + posY += multi_rx.rx_btn_height + self.sliderYs = SliderBoxV(self, 'Ys', self.y_scale, 160, self.OnChangeYscale, True) + self.sliderYz = SliderBoxV(self, 'Yz', self.y_zero, 160, self.OnChangeYzero, True) + x = self.tabX + (half_width * 2 - self.sliderYs.width - self.sliderYz.width) // 2 + self.sliderYs.SetDimension(x, posY, self.sliderYs.width, 100) + x += self.sliderYs.width + self.sliderYz.SetDimension(x, posY, self.sliderYz.width, 100) + # Create menu + self.multi_rx_menu = wx.Menu() + item = self.multi_rx_menu.Append(-1, 'Show graph') + self.Bind(wx.EVT_MENU, self.OnShowGraph, item) + item = self.multi_rx_menu.Append(-1, 'Show waterfall') + self.Bind(wx.EVT_MENU, self.OnShowWaterfall, item) + self.multi_rx_menu.AppendSeparator() + menu = wx.Menu() + self.multi_rx_menu.AppendSubMenu(menu, "Band") + for band in conf.bandLabels: + if not isinstance(band, Q3StringTypes): + band = band[0] + if band == 'Time': + continue + item = menu.Append(-1, band) + self.Bind(wx.EVT_MENU, self.OnChangeBand, item) + self.mode_menu = wx.Menu() + self.multi_rx_menu.AppendSubMenu(self.mode_menu, "Mode") + for mode in self.the_modes: + item = self.mode_menu.AppendRadioItem(-1, mode) + self.Bind(wx.EVT_MENU, self.OnChangeMode, item) + self.filter_menu = wx.Menu() + self.multi_rx_menu.AppendSubMenu(self.filter_menu, "Filter") + for i in range(6): + item = self.filter_menu.AppendRadioItem(-1, '0') + self.Bind(wx.EVT_MENU, self.OnChangeFilter, item) + self.multi_rx_menu.AppendSeparator() + item = self.multi_rx_menu.Append(-1, 'Delete receiver') + self.Bind(wx.EVT_MENU, self.OnDeleteReceiver, item) + self.ChangeBand(application.lastBand) + if multi_rx.rx_zero == multi_rx.waterfall: + self.OnShowWaterfall() + def ResizeGraph(self): + GraphScreen.ResizeGraph(self) + w, h = self.GetClientSize() + x, y = self.sliderYs.GetPosition() + height = max(h - y, self.sliderYs.text_height * 2) + self.sliderYs.SetDimension(x, y, self.sliderYs.width, height) + x, y = self.sliderYz.GetPosition() + self.sliderYz.SetDimension(x, y, self.sliderYz.width, height) + def MakeYTicks(self, dc): + if self.display == self.graph_display: + GraphScreen.MakeYTicks(self, dc) + def OnPopButton(self, event): + pos = (self.popupX, 10) + self.PopupMenu(self.multi_rx_menu, pos) + def OnDeleteReceiver(self, event): + if self.is_playing: + QS.set_multirx_play_channel(-1) + application.multi_rx_screen.DeleteReceiver(self) + def OnShowGraph(self, event): + self.waterfall_display.Hide() + self.display = self.graph_display + self.SetTxFreq(self.txFreq, self.txFreq) + self.sliderYs.SetValue(self.y_scale) + self.sliderYz.SetValue(self.y_zero) + self.display.Show() + self.doResize = True + def OnShowWaterfall(self, event=None): + self.graph_display.Hide() + self.display = self.waterfall_display + self.SetTxFreq(self.txFreq, self.txFreq) + self.sliderYs.SetValue(self.waterfall_y_scale) + self.sliderYz.SetValue(self.waterfall_y_zero) + self.display.Show() + self.doResize = True + def OnGraphData(self, data): + self.raw_graph_data = data + if self.display == self.graph_display: + self.display.OnGraphData(data) + else: + self.display.OnGraphData(data, self.waterfall_y_zero, self.waterfall_y_scale) + def OnPlayButton(self, event): + application.multi_rx_screen.StopPlaying(self) + self.is_playing = event.GetEventObject().GetValue() + if self.is_playing: + QS.set_filters(self.filter_I, self.filter_Q, self.filter_bandwidth, 0, 1) + QS.set_multirx_play_channel(self.multirx_index) + else: + QS.set_multirx_play_channel(-1) + def SetVFO(self, vfo): + GraphScreen.SetVFO(self, vfo) + self.waterfall_display.VFO = self.VFO + self.waterfall_display.Refresh() + def OnChangeBand(self, event): + idd = event.GetId() + band = event.GetEventObject().GetLabel(idd) + self.ChangeBand(band) + def OnChangeMode(self, event=None): + if event is None: + try: + idx = self.the_modes.index(self.mode) + except ValueError: + self.mode = 'USB' + idx = self.the_modes.index(self.mode) + self.mode_menu.FindItemByPosition(idx).Check(True) + else: + idd = event.GetId() + self.mode = event.GetEventObject().GetLabel(idd) + bws = application.Mode2Filters(self.mode) + self.mode_index = Mode2Index.get(self.mode, 3) + QS.set_multirx_mode(self.multirx_index, self.mode_index) + for i in range(6): + item = self.filter_menu.FindItemByPosition(i) + item.SetItemLabel(str(bws[i])) + if i == 2: + item.Check(True) + self.filter_bandwidth = bws[2] + self.OnChangeFilter() + def OnChangeFilter(self, event=None): + if event is not None: + idd = event.GetId() + self.filter_bandwidth = int(event.GetEventObject().GetLabel(idd)) + center = application.GetFilterCenter(self.mode, self.filter_bandwidth) + frate = QS.get_filter_rate(Mode2Index.get(self.mode, 3), self.filter_bandwidth) + self.filter_I, self.filter_Q = application.MakeFilterCoef(frate, None, self.filter_bandwidth, center) + if self.is_playing: + QS.set_filters(self.filter_I, self.filter_Q, self.filter_bandwidth, 0, 1) # filter for receiver that is playing sound + if self.multirx_index == 0: + QS.set_filters(self.filter_I, self.filter_Q, self.filter_bandwidth, 0, 2) # filter for digital mode output to sound device + self.filter_mode = self.mode + self.filter_center = center + def ChangeBand(self, band): + self.band = band + try: + vfo, tune, self.mode = application.bandState[band] + #print (vfo, tune, self.mode) + except: + try: + f1, f2 = conf.BandEdge[band] + except KeyError: + f1, f2 = 10000000, 12000000 + vfo = (f1 + f2) // 2 + vfo = vfo // 10000 + vfo *= 10000 + if vfo < 9000000: + self.mode = 'LSB' + else: + self.mode = 'USB' + tune = 0 + self.OnChangeMode() + self.ChangeHwFrequency(tune, vfo, 'ChangeBand') + if hasattr(application.Hardware, "ChangeBandFilters"): + application.Hardware.ChangeBandFilters() + def OnBtnDownBand(self, event): + self.OnBtnUpBand(event, True) + def OnBtnUpBand(self, event, is_band_down=False): + sample_rate = application.sample_rate + btn = event.GetEventObject() + oldvfo = self.VFO + if btn.direction > 0: # left button was used, move a bit + d = int(sample_rate // 9) + else: # right button was used, move to edge + d = int(sample_rate * 45 // 100) + if is_band_down: + d = -d + vfo = self.VFO + d + if sample_rate > 40000: + vfo = (vfo + 5000) // 10000 * 10000 # round to even number + delta = 10000 + elif sample_rate > 5000: + vfo = (vfo + 500) // 1000 * 1000 + delta = 1000 + else: + vfo = (vfo + 50) // 100 * 100 + delta = 100 + if oldvfo == vfo: + if is_band_down: + d = -delta + else: + d = delta + else: + d = vfo - oldvfo + self.ChangeHwFrequency(self.txFreq - d, self.VFO + d, 'BandUpDown', event=event) + def OnChangeYscale(self, event): + y_scale = self.sliderYs.GetValue() + if self.display == self.graph_display: + self.ChangeYscale(y_scale) + else: + self.waterfall_y_scale = y_scale + def OnChangeYzero(self, event): + y_zero = self.sliderYz.GetValue() + if self.display == self.graph_display: + self.ChangeYzero(y_zero) + else: + self.waterfall_y_zero = y_zero + def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None, rx_freq=None): + #print ("Quisk", self.multirx_index, self.VFO, vfo, self.band) + self.SetTxFreq(tune, tune) + if self.VFO != vfo: + self.SetVFO(vfo) + Hardware.MultiRxFrequency(self.multirx_index, vfo, self.band) + QS.set_multirx_freq(self.multirx_index, tune) + def ChangeRxTxFrequency(self, freq): + tune = freq - self.VFO + d = self.sample_rate * 45 // 100 + if -d <= tune <= d: # Frequency is on-screen + vfo = self.VFO + else: # Change the VFO + vfo = (freq // 5000) * 5000 - 5000 + tune = freq - vfo + self.ChangeHwFrequency(tune, vfo, 'FreqEntry') + +class MultiReceiverScreen(wx.SplitterWindow): + # The top level screen showing a graph, waterfall and any additional receivers. + # The first receiver is zero; additional receivers are in self.receiver_list[] + def __init__(self, frame, data_width, graph_width): + application.multi_rx_screen = self # prevent phase error + self.data_width = data_width + self.graph_width = graph_width + wx.SplitterWindow.__init__(self, frame) + self.SetSashGravity(0.50) + self.receiver_list = [] + self.graph = GraphScreen(self, data_width, graph_width) + self.width = self.graph.width + self.waterfall = WaterfallScreen(self, self.width, data_width, graph_width) + self.rx_zero = self.graph + self.Initialize(self.rx_zero) + self.waterfall.Hide() + self.SetSizeHints(self.width, -1, self.width) + # Calculate control width + rx_btn = QuiskPushbutton(self, None, "Rx 8....", style=wx.BU_EXACTFIT) + self.rx_btn_width, self.rx_btn_height = rx_btn.GetSize().Get() + self.rx_btn_width *= 2 + rx_btn.Destroy() + del rx_btn + self.SetMinimumPaneSize(self.rx_btn_height) + self.rx_btn_border = 5 + width = data_width - self.rx_btn_width - self.rx_btn_border * 2 + self.rx_data_width = fftPreferedSizes[0] + for x in fftPreferedSizes: + if x >= width: + break + else: + self.rx_data_width = x + def __getattr__(self, name): + return getattr(self.rx_zero, name) + def ChangeRxZero(self, show_graph): + if self.IsSplit(): + old = self.GetWindow2() + else: + old = self.GetWindow1() + if show_graph: + new = self.graph + else: + new = self.waterfall + if old != new: + self.ReplaceWindow(old, new) + new.Show() + old.Hide() + self.rx_zero = new + def StopPlaying(self, excpt): # change to not playing on all panes except excpt + for pane in self.receiver_list: + if pane != excpt: + pane.play_btn.SetValue(False) + pane.is_playing = False + def OnAddReceiver(self, event): + index = len(self.receiver_list) + if index >= 7: + return + if index == 0: + pane2 = self.rx_zero + splitter = pane2.GetParent() + pane1 = MultiRxGraph(self, self.data_width, self.graph_width, index) + self.receiver_list.append(pane1) + splitter.SplitHorizontally(pane1, self.rx_zero) + else: + pane2 = self.receiver_list[-1] + parent = pane2.GetParent() + splitter = wx.SplitterWindow(parent) + splitter.SetSizeHints(self.width, -1, self.width) + splitter.SetMinimumPaneSize(self.rx_btn_height) + splitter.SetSashGravity(0.50) + pane1 = MultiRxGraph(splitter, self.data_width, self.graph_width, index) + self.receiver_list.append(pane1) + pane2.Reparent(splitter) + parent.ReplaceWindow(pane2, splitter) + splitter.SplitHorizontally(pane1, pane2) + self.SizeEqually() + index += 1 # len(self.receiver_list) + Hardware.MultiRxCount(index) + pane1.ChangeBand(application.lastBand) + def DeleteReceiver(self, pane): + Hardware.MultiRxCount(len(self.receiver_list) - 1) + if len(self.receiver_list) == 1: + self.Unsplit(pane) + self.receiver_list.remove(pane) + del pane + elif pane in self.receiver_list[-2:]: + self.receiver_list.remove(pane) + splitter2 = pane.GetParent() + splitter1 = splitter2.GetParent() + del pane + pane = self.receiver_list[-1] + pane.Reparent(splitter1) + splitter1.ReplaceWindow(splitter2, pane) + splitter2.Destroy() + else: + self.receiver_list.remove(pane) + splitter2 = pane.GetParent() + splitter1 = splitter2.GetParent() + splitter3 = splitter2.GetWindow1() + del pane + splitter3.Reparent(splitter1) + splitter1.ReplaceWindow(splitter2, splitter3) + splitter2.Destroy() + index = 0 + for pane in self.receiver_list: + pane.multirx_index = index + pane.rx_btn.SetLabel("Rx %d" % (index + 1)) + QS.set_multirx_mode(index, pane.mode_index) + QS.set_multirx_freq(index, pane.txFreq) + index += 1 + if hasattr(application.Hardware, "ChangeBandFilters"): + application.Hardware.ChangeBandFilters() + def SizeEqually(self): + w, h = self.GetClientSize() + num = len(self.receiver_list) + self.SetSashPosition(h * num // (num + 1)) + for rx in self.receiver_list[:-1]: + splitter = rx.GetParent() + w, h = splitter.GetClientSize() + num -= 1 + splitter.SetSashPosition(h * num // (num + 1)) + def OnIdle(self, event): + self.rx_zero.OnIdle(event) + for pane in self.receiver_list: + pane.OnIdle(event) + def OnGraphData(self, data, index=None): + if index is None: # data is for the principal receiver + self.waterfall.OnGraphData(data) # Save data for switch to waterfall + if self.rx_zero == self.graph: + self.graph.OnGraphData(data) + elif index < len(self.receiver_list): + self.receiver_list[index].OnGraphData(data) + def ChangeSampleRate(self, rate): + self.graph.sample_rate = rate + self.waterfall.pane1.sample_rate = rate + self.waterfall.pane2.sample_rate = rate + self.waterfall.pane2.display.sample_rate = rate + for pane in self.receiver_list: + pane.sample_rate = rate + tune = pane.txFreq + vfo = pane.VFO + pane.txFreq = pane.VFO = -1 # demand change + pane.ChangeHwFrequency(tune, vfo, 'NewDecim') + +class ScopeScreen(wx.Window): + """Create an oscilloscope screen (mostly used for debug).""" + def __init__(self, parent, width, data_width, graph_width): + wx.Window.__init__(self, parent, pos = (0, 0), + size=(width, -1), style = wx.NO_BORDER) + self.SetBackgroundColour(conf.color_graph) + self.font = wx.Font(conf.config_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL, + wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + self.SetFont(self.font) + self.Bind(wx.EVT_SIZE, self.OnSize) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID) + self.y_scale = conf.scope_y_scale + self.y_zero = conf.scope_y_zero + self.zoom_control = 0 + self.yscale = 1 + self.running = 1 + self.doResize = False + self.width = width + self.height = 100 + self.originY = self.height // 2 + self.data_width = data_width + self.graph_width = graph_width + w = self.charx = self.GetCharWidth() + h = self.chary = self.GetCharHeight() + tick = max(2, h * 3 // 10) + self.originX = w * 3 + self.width = self.originX + self.graph_width + tick + self.charx * 2 + self.line = [(0,0), (1,1)] # initial fake graph data + self.fpout = None #open("jim96.txt", "w") + def OnIdle(self, event): + if self.doResize: + self.ResizeGraph() + def OnSize(self, event): + self.doResize = True + event.Skip() + def ResizeGraph(self, event=None): + # Change the height of the graph. Changing the width interactively is not allowed. + w, h = self.GetClientSize() + self.height = h + self.originY = h // 2 + self.doResize = False + self.Refresh() + def OnPaint(self, event): + dc = wx.PaintDC(self) + dc.SetFont(self.font) + dc.SetTextForeground(conf.color_graphlabels) + self.MakeYTicks(dc) + self.MakeXTicks(dc) + self.MakeText(dc) + dc.SetPen(wx.Pen(conf.color_graphline, 1)) + dc.DrawLines(self.line) + def MakeYTicks(self, dc): + chary = self.chary + originX = self.originX + x3 = self.x3 = originX + self.graph_width # end of graph data + dc.SetPen(wx.Pen(conf.color_graphticks,1)) + dc.DrawLine(originX, 0, originX, self.originY * 3) # y axis + # Find the size of the Y scale markings + themax = 2.5e9 * 10.0 ** - ((160 - self.y_scale) / 50.0) # value at top of screen + themax = int(themax) + l = [] + for j in (5, 6, 7, 8): + for i in (1, 2, 5): + l.append(i * 10 ** j) + for yvalue in l: + n = themax // yvalue + 1 # Number of lines + ypixels = self.height // n + if n < 20: + break + dc.SetPen(self.horizPen) + for i in range(1, 1000): + y = self.originY - ypixels * i + if y < chary: + break + # Above axis + dc.DrawLine(originX, y, x3, y) # y line + # Below axis + y = self.originY + ypixels * i + dc.DrawLine(originX, y, x3, y) # y line + self.yscale = float(ypixels) / yvalue + self.yvalue = yvalue + def MakeXTicks(self, dc): + originY = self.originY + x3 = self.x3 + # Draw the X axis + dc.SetPen(wx.Pen(conf.color_graphticks,1)) + dc.DrawLine(self.originX, originY, x3, originY) + # Find the size of the X scale markings in microseconds + for i in (20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000): + xscale = i # X scale in microseconds + if application.sample_rate * xscale * 0.000001 > self.width // 30: + break + # Draw the X lines + dc.SetPen(self.horizPen) + for i in range(1, 999): + x = int(self.originX + application.sample_rate * xscale * 0.000001 * i + 0.5) + if x > x3: + break + dc.DrawLine(x, 0, x, self.height) # x line + self.xscale = xscale + def MakeText(self, dc): + if self.running: + t = " RUN" + else: + t = " STOP" + if self.xscale >= 1000: + t = "%s X: %d millisec/div" % (t, self.xscale // 1000) + else: + t = "%s X: %d microsec/div" % (t, self.xscale) + t = "%s Y: %.0E/div" % (t, self.yvalue) + dc.DrawText(t, self.originX, self.height - self.chary) + def OnGraphData(self, data): + if not self.running: + if self.fpout: + for cpx in data: + re = int(cpx.real) + im = int(cpx.imag) + ab = int(abs(cpx)) + ph = math.atan2(im, re) * 360. / (2.0 * math.pi) + self.fpout.write("%12d %12d %12d %12.1d\n" % (re, im, ab, ph)) + return # Preserve data on screen + line = [] + x = self.originX + ymax = self.height + for cpx in data: # cpx is complex raw samples +/- 0 to 2**31-1 + y = cpx.real + #y = abs(cpx) + y = self.originY - int(y * self.yscale + 0.5) + if y > ymax: + y = ymax + elif y < 0: + y = 0 + line.append((x, y)) + x = x + 1 + self.line = line + self.Refresh() + def ChangeYscale(self, y_scale): + self.y_scale = y_scale + self.doResize = True + def ChangeYzero(self, y_zero): + self.y_zero = y_zero + def SetTxFreq(self, tx_freq, rx_freq): + pass + +class BandscopeScreen(WaterfallScreen): + def __init__(self, frame, width, data_width, graph_width, clock): + self.zoom = 1.0 + self.zoom_deltaf = 0 + self.zoom_control = 0 + WaterfallScreen.__init__(self, frame, width, data_width, graph_width) + self.sample_rate = self.pane1.sample_rate = self.pane2.sample_rate = int(clock) // 2 + self.VFO = clock // 4 + self.SetVFO(self.VFO) + def SetTxFreq(self, tx_freq, rx_freq): + freq = tx_freq + application.VFO - self.VFO + WaterfallScreen.SetTxFreq(self, freq, freq) + def SetFrequency(self, freq): # freq is 7000000, not the offset from VFO + freq = freq - self.VFO + WaterfallScreen.SetTxFreq(self, freq, freq) + def ChangeZoom(self, zoom_control): # zoom_control is the slider value 0 to 1000 + self.zoom_control = zoom_control + if zoom_control < 50: + zoom = 1.0 + zoom_deltaf = 0 + else: + zoom = 1.0 - zoom_control / 1000.0 * 0.95 + freq = application.rxFreq + application.VFO + srate = int(self.sample_rate * zoom) # reduced (zoomed) sample rate + if freq - srate // 2 < 0: + zoom_deltaf = srate // 2 - self.VFO + elif freq + srate // 2 > self.sample_rate: + zoom_deltaf = self.VFO - srate // 2 + else: + zoom_deltaf = freq - self.VFO + self.zoom = zoom + self.zoom_deltaf = zoom_deltaf + self.pane1.ChangeZoom(zoom, zoom_deltaf, zoom_control) + self.pane2.ChangeZoom(zoom, zoom_deltaf, zoom_control) + self.pane2.display.ChangeZoom(zoom, zoom_deltaf, zoom_control) + +class FilterScreen(GraphScreen): + """Create a graph of the receive filter response.""" + def __init__(self, parent, data_width, graph_width): + GraphScreen.__init__(self, parent, data_width, graph_width) + self.y_scale = conf.filter_y_scale + self.y_zero = conf.filter_y_zero + self.zoom_control = 0 + self.VFO = 0 + self.txFreq = 0 + self.data = [] + self.sample_rate = QS.get_filter_rate(-1, -1) + def NewFilter(self): + self.sample_rate = QS.get_filter_rate(-1, -1) + self.data = QS.get_filter() + mx = -1000 + for x in self.data: + if mx < x: + mx = x + mx -= 3.0 + f1 = None + for i in range(len(self.data)): + x = self.data[i] + if x > mx: + if f1 is None: + f1 = i + f2 = i + bw3 = float(f2 - f1) / len(self.data) * self.sample_rate + mx -= 3.0 + f1 = None + for i in range(len(self.data)): + x = self.data[i] + if x > mx: + if f1 is None: + f1 = i + f2 = i + bw6 = float(f2 - f1) / len(self.data) * self.sample_rate + self.display.display_text = "Filter 3 dB bandwidth %.0f, 6 dB %.0f" % (bw3, bw6) + #self.data = QS.get_tx_filter() + self.doResize = True + def OnGraphData(self, data): + GraphScreen.OnGraphData(self, self.data) + def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None, rx_freq=None): + GraphScreen.SetTxFreq(self, tune, tune) + application.freqDisplay.Display(tune) + def SetTxFreq(self, tx_freq, rx_freq): + pass + +class AudioFFTScreen(GraphScreen): + """Create an FFT graph of the transmit audio.""" + def __init__(self, parent, data_width, graph_width, sample_rate): + GraphScreen.__init__(self, parent, data_width, graph_width) + self.y_scale = conf.filter_y_scale + self.y_zero = conf.filter_y_zero + self.zoom_control = 0 + self.VFO = 0 + self.txFreq = 0 + self.sample_rate = sample_rate + def OnGraphData(self, data): + GraphScreen.OnGraphData(self, data) + def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None, rx_freq=None): + GraphScreen.SetTxFreq(self, tune, tune) + application.freqDisplay.Display(tune) + def SetTxFreq(self, tx_freq, rx_freq): + pass + +class HelpScreen(wx.html.HtmlWindow): + """Create the screen for the Help button.""" + def __init__(self, parent, width, height): + wx.html.HtmlWindow.__init__(self, parent, -1, size=(width, height)) + self.y_scale = 0 + self.y_zero = 0 + self.zoom_control = 0 + if "gtk2" in wx.PlatformInfo: + self.SetStandardFonts() + self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22]) + # read in text from file help.html in the directory of this module + self.LoadFile('help.html') + def OnGraphData(self, data): + pass + def ChangeYscale(self, y_scale): + pass + def ChangeYzero(self, y_zero): + pass + def OnIdle(self, event): + pass + def SetTxFreq(self, tx_freq, rx_freq): + pass + def OnLinkClicked(self, link): + webbrowser.open(link.GetHref(), new=2) + +class QMainFrame(wx.Frame): + """Create the main top-level window.""" + def __init__(self, width, height): + fp = open('__init__.py') # Read in the title + self.title = fp.readline().strip()[1:] + fp.close() + x = conf.window_posX + y = conf.window_posY + wx.Frame.__init__(self, None, -1, self.title, (x, y), + (width, height), wx.DEFAULT_FRAME_STYLE, 'MainFrame') + self.SetBackgroundColour(conf.color_bg) + self.SetForegroundColour(conf.color_bg_txt) + self.Bind(wx.EVT_CLOSE, self.OnBtnClose) + if DEBUGSHELL: + #debugshell = CrustFrame() + debugshell = ShellFrame(parent=self) + debugshell.Show() + debugshell.shell.write("hw=quisk.application.Hardware") + def OnBtnClose(self, event): + application.OnBtnClose(event) + self.Destroy() + def SetConfigText(self, text): + if len(text) > 100: + text = text[0:80] + '|||' + text[-17:] + self.SetTitle("Radio %s %s %s" % (application.local_conf.RadioName, self.title, text)) + +class Spacer(wx.Window): + """Create a bar between the graph screen and the controls""" + def __init__(self, parent): + wx.Window.__init__(self, parent, pos = (0, 0), + size=(-1, 6), style = wx.NO_BORDER) + self.Bind(wx.EVT_PAINT, self.OnPaint) + r, g, b = parent.GetBackgroundColour().Get(False) + dark = (r * 7 // 10, g * 7 // 10, b * 7 // 10) + light = (r + (255 - r) * 5 // 10, g + (255 - g) * 5 // 10, b + (255 - b) * 5 // 10) + self.dark_pen = wx.Pen(dark, 1, wx.SOLID) + self.light_pen = wx.Pen(light, 1, wx.SOLID) + self.width = application.screen_width + def OnPaint(self, event): + dc = wx.PaintDC(self) + w = self.width + dc.SetPen(self.dark_pen) + dc.DrawLine(0, 0, w, 0) + dc.DrawLine(0, 1, w, 1) + dc.DrawLine(0, 2, w, 2) + dc.SetPen(self.light_pen) + dc.DrawLine(0, 3, w, 3) + dc.DrawLine(0, 4, w, 4) + dc.DrawLine(0, 5, w, 5) + +class App(wx.App): + """Class representing the application.""" + StateNames = [ # Names of state attributes to save and restore + 'bandState', 'bandAmplPhase', 'lastBand', 'VFO', 'txFreq', 'mode', + 'vardecim_set', 'filterAdjBw1', 'levelAGC', 'levelOffAGC', 'volumeAudio', 'levelSpot', + 'levelSquelch', 'levelSquelchSSB', 'levelVOX', 'timeVOX', 'sidetone_volume', + 'txAudioClipUsb', 'txAudioClipAm','txAudioClipFm', 'txAudioClipFdv', + 'txAudioPreemphUsb', 'txAudioPreemphAm', 'txAudioPreemphFm', 'txAudioPreemphFdv', + 'wfallScaleZ', 'wfallGrScaleZ', 'graphScaleZ', 'split_rxtx_play', 'modeFilter', + 'file_name_rec_audio', 'file_name_rec_samples', 'file_name_rec_mic', 'file_name_rec_tmp', + 'file_name_play_audio', 'file_name_play_samples', 'file_name_play_cq', 'freedv_mode', + 'file_play_source', 'hermes_LNA_dB'] + def __init__(self): + global application + QS.AppStatus(1) + application = self + self.bottom_widgets = None + self.dxCluster = None + self.main_frame = None + self.remote_control_head = False + self.remote_control_slave = False + self.keys_down = [] + wx.App.__init__(self) + def QuiskText(self, *args, **kw): # Make our text control available to widget files + return QuiskText(*args, **kw) + def QuiskText1(self, *args, **kw): # Make our text control available to widget files + return QuiskText1(*args, **kw) + def QuiskPushbutton(self, *args, **kw): # Make our buttons available to widget files + return QuiskPushbutton(*args, **kw) + def QuiskRepeatbutton(self, *args, **kw): + return QuiskRepeatbutton(*args, **kw) + def QuiskCheckbutton(self, *args, **kw): + return QuiskCheckbutton(*args, **kw) + def QuiskCycleCheckbutton(self, *args, **kw): + return QuiskCycleCheckbutton(*args, **kw) + def RadioButtonGroup(self, *args, **kw): + return RadioButtonGroup(*args, **kw) + def SliderBoxHH(self, *args, **kw): + return SliderBoxHH(*args, **kw) + def FilterEvent(self, event): + try: # I am not sure if older wxPython can do this + typ = event.GetEventType() + if typ == wx.EVT_KEY_DOWN.typeId: + key = event.GetKeyCode() # This is always upper case for letters + if key not in self.keys_down: + self.keys_down.append(key) + #print (self.keys_down) + elif typ == wx.EVT_KEY_UP.typeId: + key = event.GetKeyCode() + self.keys_down.remove(key) + #print (self.keys_down) + except: + self.keys_down = [] + return -1 + def QuiskGetKeyState(self, key): # Replace normal wx.GetKeyState() because it only works with Shift, Control, Alt + if 97 <= key <= 122: # key is an integer; convert to upper case + key -= 32 + return key in self.keys_down + def OnInit(self): + """Perform most initialization of the app here (called by wxPython on startup).""" + wx.lib.colourdb.updateColourDB() # Add additional color names + import quisk_widgets # quisk_widgets needs the application object + quisk_widgets.application = self + del quisk_widgets + global conf # conf is the module for all configuration data + setattr(conf, 'config_file_path', ConfigPath) + setattr(conf, 'DefaultConfigDir', DefaultConfigDir) + if os.path.isfile(ConfigPath): # See if the user has a config file + setattr(conf, 'config_file_exists', True) + d = {} + d.update(conf.__dict__) # make items from conf available + exec(compile(open(ConfigPath).read(), ConfigPath, 'exec'), d) # execute the user's config file + if os.path.isfile(ConfigPath2): # See if the user has a second config file + exec(compile(open(ConfigPath2).read(), ConfigPath2, 'exec'), d) # execute the user's second config file + for k in d: # add user's config items to conf + v = d[k] + if k[0] != '_': # omit items starting with '_' + setattr(conf, k, v) + else: + setattr(conf, 'config_file_exists', False) + self.QuiskFilesDir = os.path.dirname(conf.settings_file_path) # directory for Quisk files + if not os.path.isdir(self.QuiskFilesDir): + self.QuiskFilesDir = DefaultConfigDir + self.std_out_err = StdOutput(self) + QS.set_params(quisk_is_vna=0) # We are not the VNA program + # Read in configuration from the selected radio + self.BandPlan = [] + if self.main_frame: + self.local_conf = configure.Configuration(self, 'Same') + else: + self.local_conf = configure.Configuration(self, argv_options.AskMe, argv_options.radio) + self.local_conf.UpdateConf() + # Choose whether to use Unicode or text symbols + for k in ('sym_stat_mem', 'sym_stat_fav', 'sym_stat_dx', + 'btn_text_range_dn', 'btn_text_range_up', 'btn_text_play', 'btn_text_rec', 'btn_text_file_rec', + 'btn_text_file_play', 'btn_text_fav_add', + 'btn_text_fav_recall', 'btn_text_mem_add', 'btn_text_mem_next', 'btn_text_mem_del'): + if conf.use_unicode_symbols: + setattr(conf, 'X' + k, getattr(conf, 'U' + k)) + else: + setattr(conf, 'X' + k, getattr(conf, 'T' + k)) + MakeWidgetGlobals() + if conf.invertSpectrum: + QS.invert_spectrum(1) + if conf.use_sdriq: + sample_rate = int(66666667.0 / conf.sdriq_decimation + 0.5) + if conf.use_sdriq or conf.use_rx_udp: + name_of_sound_capt = '' + name_of_mic_play = '' + self.wfallScaleZ = {} # scale and zero for the waterfall pane2 + self.wfallGrScaleZ = {} # scale and zero for the waterfall graph pane1 + self.graphScaleZ = {} # scale and zero for the graph + self.bandState = {} # for key band, the current (self.VFO, self.txFreq, self.mode) + self.bandState.update(conf.bandState) + self.memoryState = [] # a list of (freq, band, self.VFO, self.txFreq, self.mode) + self.bandAmplPhase = {} + self.samples_from_python = False + self.NewIdList = [] # Hack: list of Ids in use for accelerator table + self.midi_message = [] + self.idName2Button = {} + self.midi_handler = None + if conf.use_rx_udp == 10: # Hermes UDP protocol + self.bandscope_clock = conf.rx_udp_clock + else: + self.bandscope_clock = 0 + self.modeFilter = { # the filter button index in use for each mode + 'CW' : 3, + 'SSB' : 3, + 'AM' : 3, + 'FM' : 3, + 'DGT' : 1, + 'FDV' : 2, + 'IMD' : 3, + conf.add_extern_demod : 3, + } + if sys.platform == 'win32' and (conf.hamlib_com1_name or conf.hamlib_com2_name): + try: # make sure the pyserial module exists + import serial + except: + dlg = wx.MessageDialog(None, "The Python pyserial module is required but not installed. Do you want me to install it?", + "Install Python pyserial", style = wx.YES|wx.NO) + if dlg.ShowModal() == wx.ID_YES: + subprocess.call([sys.executable, "-m", "pip", "install", "pyserial"]) + try: + import serial + except: + dlg = wx.MessageDialog(None, "Installation of Python pyserial failed. Please install it by hand.", + "Installation failed", style=wx.OK) + dlg.ShowModal() + # Open hardware file + global Hardware + if self.local_conf.GetHardware(): + pass + else: + if hasattr(conf, "Hardware"): # Hardware defined in config file + self.Hardware = conf.Hardware(self, conf) + hname = ConfigPath + else: + self.Hardware = conf.quisk_hardware.Hardware(self, conf) + hname = conf.quisk_hardware.__file__ + if hname[-3:] == 'pyc': + hname = hname[0:-1] + setattr(conf, 'hardware_file_name', hname) + if conf.quisk_widgets: + hname = conf.quisk_widgets.__file__ + if hname[-3:] == 'pyc': + hname = hname[0:-1] + setattr(conf, 'widgets_file_name', hname) + else: + setattr(conf, 'widgets_file_name', '') + Hardware = self.Hardware + self.use_fast_heart_beat = hasattr(Hardware, "FastHeartBeat") + # Initialization - may be over-written by persistent state + self.local_conf.Initialize() + self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow + self.smeter_db_count = 0 # average the S-meter + self.smeter_db_sum = 0 + self.smeter_db = 0 + self.smeter_avg_seconds = 1.0 # seconds for S-meter average + self.smeter_sunits = -87.0 + self.smeter_usage = "smeter" # specify use of s-meter display + self.timer = time.time() # A seconds clock + self.heart_time0 = self.timer # timer to call HeartBeat at intervals + self.save_time0 = self.timer + self.smeter_db_time0 = self.timer + self.smeter_sunits_time0 = self.timer + self.fewsec_time0 = self.timer + self.multi_rx_index = 0 + self.multi_rx_timer = self.timer + self.band_up_down = 0 # Are band Up/Down buttons in use? + self.lastBand = 'Audio' + self.filterAdjBw1 = 1000 + self.levelAGC = 500 # AGC level ON control, 0 to 1000 + self.levelOffAGC = 100 # AGC level OFF control, 0 to 1000 + self.levelSquelch = 500 # FM squelch level, 0 to 1000 + self.levelSquelchSSB = 200 # SSB squelch level, 0 to 1000 + self.levelVOX = -20 # audio level that triggers VOX + self.timeVOX = 500 # hang time for VOX + self.useVOX = False # Is the VOX button down? + self.txAudioClipUsb = 5 # Tx audio clip level in dB + self.txAudioClipAm = 0 + self.txAudioClipFm = 0 + self.txAudioClipFdv = 0 + self.txAudioPreemphUsb = 70 # Tx audio preemphasis 0 to 100 + self.txAudioPreemphAm = 0 + self.txAudioPreemphFm = 0 + self.txAudioPreemphFdv = 0 + self.levelSpot = 500 # Spot level control, 0 to 1000 + self.volumeAudio = 300 # audio volume + self.VFO = 0 # frequency of the VFO + self.ritFreq = 0 # receive incremental tuning frequency offset + self.txFreq = 0 # Transmit frequency as +/- sample_rate/2 + self.rxFreq = 0 # Receive frequency as +/- sample_rate/2 + self.tx_level = 100 # initially 100%; Caution: there is also a conf.tx_level dictionary + self.digital_tx_level = conf.digital_tx_level + self.fft_size = 1 + self.accel_list = [] + self.old_RxTx = 0 + self.wsjtx_process = [None] * 10 # Maximum of 10 receivers + self.config_midi_window = None + self.hot_key_ptt_is_down = False + self.hot_key_ptt_was_down = False + self.hot_key_ptt_pressed = False + self.hot_key_ptt_active = False + self.serial_ptt_active = False + self.vox_ptt_active = False + self.freedv_mode = 'Mode 700D' # restore FreeDV mode setting + self.freedv_menu = None + self.hermes_LNA_dB = 20 + if hasattr(Hardware, "OnChangeRxTx"): + self.want_RxTx = True + else: + self.want_RxTx = False + if conf.do_repeater_offset and hasattr(Hardware, "RepeaterOffset"): + QS.tx_hold_state(1) + # Quisk control by Hamlib through a serial port + if conf.hamlib_com1_name: + self.hamlib_com1_handler = HamlibHandlerSerial(self, conf.hamlib_com1_name) + else: + self.hamlib_com1_handler = None + if conf.hamlib_com2_name: + self.hamlib_com2_handler = HamlibHandlerSerial(self, conf.hamlib_com2_name) + else: + self.hamlib_com2_handler = None + # Quisk control by Hamlib through rig 2 + self.hamlib_clients = [] # list of TCP connections to handle + if conf.hamlib_port: + try: + self.hamlib_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.hamlib_socket.bind((conf.hamlib_ip, conf.hamlib_port)) + self.hamlib_socket.settimeout(0.0) + self.hamlib_socket.listen(5) # listen for TCP connections from multiple clients + except: + self.hamlib_socket = None + # traceback.print_exc() + else: + self.hamlib_socket = None + # Quisk control by fldigi + self.fldigi_new_freq = None + self.fldigi_freq = None + if conf.digital_xmlrpc_url: + self.fldigi_server = ServerProxy(conf.digital_xmlrpc_url) + else: + self.fldigi_server = None + self.fldigi_rxtx = 'rx' + self.fldigi_timer = 0 + self.screen = None + # Display the audio FFT instead of the RX filter or bandscope when self.rate_audio_fft > 0. + # The sample rate is self.rate_audio_fft. Add an instance of quisk_calc_audio_graph() to C code to provide data. + self.rate_audio_fft = 0 + self.audio_fft_screen = None + self.audio_volume = 0.0 # Set output volume, 0.0 to 1.0 + self.sidetone_volume = 0 # sidetone control value 0 to 1000 + self.sidetone_0to1 = 0 # log taper sidetone volume 0.0 to 1.0 + self.sound_thread = None + self.mode = conf.default_mode + self.color_list = None + self.color_index = 0 + self.vardecim_set = 48000 + self.w_phase = None + self.zoom = 1.0 + self.filter_bandwidth = 1000 # filter bandwidth + self.zoom_deltaf = 0 + self.zooming = False + self.split_rxtx = False # Are we in split Rx/Tx mode? + self.split_locktx = False # Split mode Tx frequency is fixed. + self.split_lockrx = False # Split mode Rx frequency is fixed. + self.split_rxtx_play = 2 # Play 1=both, high on Right; 2=both, high on Left; 3=only Rx; 4=only Tx + self.split_offset = 0 # current frequency difference when using Split + self.savedState = {} + self.tx_indicator = False + self.tmp_playing = False + self.file_play_state = 0 # 0 == not playing a file, 1 == playing a file, 2 == waiting for the repeat time + self.file_play_repeat = 0 # Repeat time in seconds, or zero for no repeat + self.file_play_timer = 0 + self.file_play_source = 0 # 10 == play audio file, 11 == play I/Q sample file, 12 == play CQ message + self.file_name_rec_audio = '' + self.file_name_rec_samples = '' + self.file_name_rec_mic = '' + self.file_name_rec_tmp = '' + self.file_name_play_audio = '' + self.file_name_play_samples = '' + self.file_name_play_cq = '' + self.midiControls = {} # Control object and associated function for Midi control name + # get the screen size - thanks to Lucian Langa + x, y, self.screen_width, self.screen_height = wx.Display().GetGeometry() # Using display index 0 + self.Bind(wx.EVT_IDLE, self.OnIdle) + self.Bind(wx.EVT_QUERY_END_SESSION, self.OnEndSession) + # Restore persistent program state + state = None + path = os.path.join(self.QuiskFilesDir, 'quisk_init.json') + if os.path.isfile(path): + try: + fp = open(path, "r") + state = json.load(fp) + fp.close() + except: + traceback.print_exc() + else: # Load from obsolete location + path = os.path.join(os.path.dirname(ConfigPath), '.quisk_init.pkl') + if os.path.isfile(path): + try: + fp = open(path, "rb") # Pickle requires a bytes object + state = pickle.load(fp) + fp.close() + except: + pass #traceback.print_exc() + if state: + try: + for k in state: + v = state[k] + if k in self.StateNames: + self.savedState[k] = v + attr = getattr(self, k) + if isinstance(attr, dict): + attr.update(v) + else: + setattr(self, k, v) + except: + pass #traceback.print_exc() + if "Version" not in self.bandAmplPhase: + self.FixAmplPhase() # Convert to new format Oct 2020 + if Hardware.VarDecimGetChoices(): # Hardware can change the decimation. + self.sample_rate = Hardware.VarDecimSet() # Get the sample rate. + self.vardecim_set = self.sample_rate + try: + var_rate1, var_rate2 = Hardware.VarDecimRange() + except: + var_rate1, var_rate2 = (48000, 960000) + else: # Use the sample rate from the config file. + var_rate1 = None + self.sample_rate = conf.sample_rate + if not hasattr(conf, 'playback_rate'): + if conf.use_sdriq or conf.use_rx_udp: + conf.playback_rate = 48000 + else: + conf.playback_rate = conf.sample_rate + # Create the main frame + if conf.window_width > 0: # fixed width of the main frame + self.width = conf.window_width + else: + self.width = self.screen_width * 8 // 10 + if conf.window_height > 0: # fixed height of the main frame + self.height = conf.window_height + else: + self.height = self.screen_height * 5 // 10 + if self.main_frame: + frame = self.main_frame + szr = frame.GetSizer() + szr.Clear(True) + frame.SetSizer(None, True) + frame.SetSize(wx.Size(self.width, self.height)) + else: + self.main_frame = frame = QMainFrame(self.width, self.height) + self.SetTopWindow(frame) + self.std_out_err.Create(frame) + if conf.pulse_audio_verbose_output == 0: + self.std_out_err.Show(False) + else: + self.std_out_err.Show(True) + #w, h = frame.GetSize().Get() + #ww, hh = frame.GetClientSizeTuple() + #print ('Main frame: size', w, h, 'client', ww, hh) + # Find the data width from a list of preferred sizes; it is the width of returned graph data. + # The graph_width is the width of data_width that is displayed. + if conf.window_width > 0: + wFrame, h = frame.GetClientSize().Get() # client window width + graph = GraphScreen(frame, self.width//2, self.width//2, None) # make a GraphScreen to calculate borders + self.graph_width = wFrame - (graph.width - graph.graph_width) # less graph borders equals actual graph_width + graph.Destroy() + del graph + if self.graph_width % 2 == 1: # Both data_width and graph_width are even numbers + self.graph_width -= 1 + width = int(self.graph_width / conf.display_fraction) # estimated data width + for x in fftPreferedSizes: + if x >= width: + self.data_width = x + break + else: + self.data_width = fftPreferedSizes[-1] + else: # use conf.graph_width to determine the width + width = self.screen_width * conf.graph_width # estimated graph width + percent = conf.display_fraction # display central fraction of total width + percent = int(percent * 100.0 + 0.4) + width = width * 100 // percent # estimated data width + for x in fftPreferedSizes: + if x > width: + self.data_width = x + break + else: + self.data_width = fftPreferedSizes[-1] + self.graph_width = self.data_width * percent // 100 + if self.graph_width % 2 == 1: # Both data_width and graph_width are even numbers + self.graph_width += 1 + #print('graph_width', self.graph_width, 'data_width', self.data_width) + # The FFT size times the average_count controls the graph refresh rate + factor = float(self.sample_rate) / conf.graph_refresh / self.data_width + ifactor = int(factor + 0.5) # fft size multiplier * average count + if conf.fft_size_multiplier >= 999: # Use large FFT and average count 1 + fft_mult = ifactor + average_count = 1 + elif conf.fft_size_multiplier > 0: # Specified fft_size_multiplier + fft_mult = conf.fft_size_multiplier + average_count = int(factor / fft_mult + 0.5) + if average_count < 1: + average_count = 1 + elif var_rate1 is None: # Calculate an equal split between fft size and average + fft_mult = 1 + for mult in (32, 27, 24, 18, 16, 12, 9, 8, 6, 4, 3, 2, 1): # product of small factors + average_count = int(factor / mult + 0.5) + if average_count >= mult: + fft_mult = mult + break + average_count = int(factor / fft_mult + 0.5) + if average_count < 1: + average_count = 1 + else: # Calculate a compromise for variable rates + fft_mult = int(float(var_rate1) / conf.graph_refresh / self.data_width + 0.5) # large fft_mult at low rate + if fft_mult > 8: + fft_mult = 8 + elif fft_mult == 5: + fft_mult = 4 + elif fft_mult == 7: + fft_mult = 6 + average_count = int(factor / fft_mult + 0.5) + if average_count < 1: + average_count = 1 + self.fft_size = self.data_width * fft_mult + # Record the basic application parameters + self.multi_rx_screen = MultiReceiverScreen(frame, self.data_width, self.graph_width) + if sys.platform == 'win32': + h = self.main_frame.GetHandle() + else: + h = 0 + wisdom_path = os.path.join(self.QuiskFilesDir, 'quisk_wisdom.cache') + if conf.midi_cwkey_device: + QS.control_midi(midi_cwkey_note = conf.midi_cwkey_note) + midi = QS.control_midi(get_in_devices = 1) + if sys.platform == 'win32': + for i in range(len(midi)): + midi[i] = (midi[i], i) + found = None + for friendly, device in midi: + if conf.midi_cwkey_device == friendly: + found = device + break + if found is None: + for friendly, device in midi: + if conf.midi_cwkey_device.startswith(friendly) or friendly.startswith(conf.midi_cwkey_device): + found = device + break + if found is None: + print("Can not find MIDI device", conf.midi_cwkey_device) + elif sys.platform == 'win32': + QS.control_midi(client=found) + else: + QS.control_midi(device=found) + QS.record_app(self, conf, self.data_width, self.graph_width, self.fft_size, + self.multi_rx_screen.rx_data_width, self.sample_rate, h, wisdom_path) + # Start wdsp + self.wdsp = quisk_wdsp.Cwdsp(self) + self.wdsp_channel = 1 + self.wdsp.open(self.wdsp_channel) + #print ('data_width %d, FFT size %d, FFT mult %d, average_count %d, rate %d, Refresh %.2f Hz' % ( + # self.data_width, self.fft_size, self.fft_size / self.data_width, average_count, self.sample_rate, + # float(self.sample_rate) / self.fft_size / average_count)) + QS.record_graph(0, 0, 1.0) + QS.set_tx_audio(vox_level=20, vox_time=self.timeVOX) # Turn off VOX, set VOX time + # Make all the screens and hide all but one. MultiReceiver creates the graph and waterfall screens. + self.screen = self.multi_rx_screen + self.graph = self.multi_rx_screen.graph + self.waterfall = self.multi_rx_screen.waterfall + width = self.multi_rx_screen.graph.width + self.config_screen = ConfigScreen(frame, width, self.fft_size) + self.config_screen.Hide() + self.scope = ScopeScreen(frame, width, self.data_width, self.graph_width) + self.scope.Hide() + self.bandscope_screen = BandscopeScreen(frame, width, self.graph_width, self.graph_width, self.bandscope_clock) + self.bandscope_screen.Hide() + self.filter_screen = FilterScreen(frame, self.data_width, self.graph_width) + self.filter_screen.Hide() + if self.rate_audio_fft: + self.audio_fft_screen = AudioFFTScreen(frame, self.data_width, self.graph_width, self.rate_audio_fft) + self.audio_fft_screen.Hide() + self.help_screen = HelpScreen(frame, width, self.screen_height // 10) + self.help_screen.Hide() + self.station_screen = StationScreen(frame, width, conf.station_display_lines) + self.station_screen.Hide() + # Make a vertical box to hold all the screens and the bottom box + vertBox = self.vertBox = wx.BoxSizer(wx.VERTICAL) + frame.SetSizer(vertBox) + # Add the screens + vertBox.Add(self.config_screen, 1, wx.EXPAND) + vertBox.Add(self.multi_rx_screen, 1) + vertBox.Add(self.scope, 1) + vertBox.Add(self.bandscope_screen, 1) + vertBox.Add(self.filter_screen, 1) + if self.rate_audio_fft: + vertBox.Add(self.audio_fft_screen, 1) + vertBox.Add(self.help_screen, 1) + vertBox.Add(self.station_screen) + # Add the spacer + vertBox.Add(Spacer(frame), 0, wx.EXPAND) + # Add the sizer for the controls + gap = 2 + gbs = wx.GridBagSizer(gap, gap) + self.gbs = gbs + vertBox.Add(gbs, flag=wx.EXPAND) + gbs.SetEmptyCellSize((5, 5)) + # Add the bottom spacer + vertBox.AddSpacer(5) # Thanks to Christof, DJ4CM + # End of vertical box. + self.MakeButtons(frame, gbs) + minw = width = self.graph.width + maxw = maxh = -1 + minh = 100 + if conf.window_width > 0: + minw = width = maxw = conf.window_width + if conf.window_height > 0: + minh = maxh = self.height = conf.window_height + self.main_frame.SetSizeHints(minw, minh, maxw, maxh) + self.main_frame.SetClientSize(wx.Size(width, self.height)) + if hasattr(Hardware, 'pre_open'): # pre_open() is called before open() + Hardware.pre_open() + if self.local_conf.GetWidgets(self, Hardware, conf, frame, gbs, vertBox): + pass + elif conf.quisk_widgets: + self.bottom_widgets = conf.quisk_widgets.BottomWidgets(self, Hardware, conf, frame, gbs, vertBox) + if self.bottom_widgets: # Extend the sliders to the bottom of the screen + try: + i = self.bottom_widgets.num_rows_added # No way to get total rows until ver 2.9 !! + except: + i = 1 + rows = self.widget_row + i + for i in self.slider_columns: + item = gbs.FindItemAtPosition((0, i)) + item.SetSpan((rows, 1)) + self.OpenHardware() + msg = QS.open_key(port=conf.quisk_serial_port, cts=conf.quisk_serial_cts, dsr=conf.quisk_serial_dsr) + if msg: + print(msg) + self.OpenSound() + tune, vfo = Hardware.ReturnFrequency() # Request initial frequency + if tune is None or vfo is None: # Set last-used frequency + self.bandBtnGroup.SetLabel(self.lastBand, do_cmd=True, direction=0) + else: # Set requested frequency + self.BandFromFreq(tune) + self.ChangeDisplayFrequency(tune - vfo, vfo) + # Record filter rate for the filter screen + self.filter_screen.sample_rate = QS.get_filter_rate(-1, -1) + self.config_screen.InitBitmap() + self.screenBtnGroup.SetLabel(conf.default_screen, do_cmd=True) + frame.Show() + self.Yield() + self.sound_thread = SoundThread(self.samples_from_python) + self.sound_thread.start() + if self.samples_from_python: + Hardware.StartSamples() + if conf.dxClHost: + # create DX Cluster and register listener for change notification + self.dxCluster = dxcluster.DxCluster() + self.dxCluster.setListener(self.OnDxClChange) + self.dxCluster.start() + # Create shortcut keys for buttons + if conf.button_layout == 'Large screen': + for button in self.modeButns.GetButtons(): # mode buttons + self.idName2Button[button.idName] = button + if button.char_shortcut: + rid = self.QuiskNewId() + self.main_frame.Bind(wx.EVT_MENU, self.modeButns.Shortcut, id=rid) + self.accel_list.append(wx.AcceleratorEntry(wx.ACCEL_ALT, ord(button.char_shortcut), rid)) + for button in self.bandBtnGroup.GetButtons(): # band buttons + self.idName2Button[button.idName] = button + if button.char_shortcut: + rid = self.QuiskNewId() + self.main_frame.Bind(wx.EVT_MENU, self.bandBtnGroup.Shortcut, id=rid) + self.accel_list.append(wx.AcceleratorEntry(wx.ACCEL_ALT, ord(button.char_shortcut), rid)) + else: # Small screen + for button in self.modeButns.GetButtons(): # mode buttons + self.idName2Button[button.idName] = self.modeButns + for button in self.bandBtnGroup.GetButtons(): # band buttons + self.idName2Button[button.idName] = self.bandBtnGroup + self.main_frame.SetAcceleratorTable(wx.AcceleratorTable(self.accel_list)) + self.modeButns.SetLabel(self.mode, True) + # self.OnTestTimer(None) + if hasattr(Hardware, 'post_open'): # post_open() is called after open() and after sound is started + Hardware.post_open() + self.StartWsjtx() + self.midiControls["Tune"] = (None, None) + self.midiControls["Vol"] = (self.sliderVol, self.ChangeVolume) + self.midiControls["STo"] = (self.sliderSto, self.ChangeSidetone) + self.midiControls["Rit"] = (self.ritScale, self.OnRitScale) + self.midiControls["Ys"] = (self.sliderYs, self.ChangeYscale) + self.midiControls["Yz"] = (self.sliderYz, self.ChangeYzero) + self.midiControls["Zo"] = (self.sliderZo, self.OnChangeZoom) + bw = self.bottom_widgets + if hasattr(bw, "sliderLNA"): + self.midiControls["RfLna"] = (bw.sliderLNA, bw.OnLNA) + self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyHook) + return True + def OnStartWsjtx(self, ctrl): + method = ctrl.GetValue() + if method[-3:] == "now": + self.StartWsjtx(method, ctrl) + else: + self.local_conf.globals["start_wsjtx"] = method + self.local_conf.settings_changed = True + def StartWsjtx(self, method=None, ctrl=None): + path = self.local_conf.globals.get('path_to_wsjtx', '') + if not path: + if sys.platform == 'win32': + path = "C:\\WSJT\\wsjtx\\bin\\wsjtx.exe" + else: + path = "/usr/bin/wsjtx" + cfg = self.local_conf.globals.get('config_wsjtx', '') + rig = self.local_conf.globals.get('rig_name_wsjtx', 'quisk') + if cfg: + prog = [path, "--rig-name", rig, "--config", cfg] + else: + prog = [path, "--rig-name", rig] + if method is None: + method = self.local_conf.globals.get("start_wsjtx", "Never") + if method == "Never": + return + if not os.path.isfile(path): + self.wsjtx_wait = wx.BusyInfo("Path to WSJT-X was not found.") + wx.CallLater(5000, self.StartWsjtxDone, None) + if ctrl: + ctrl.SetText("Never") + return + if method == "Main Rx0 on startup": + self.wsjtx_process[0] = subprocess.Popen(prog, shell=False) + elif method == "Main Rx0 now": + if self.wsjtx_process[0] is None or self.wsjtx_process[0].poll() is not None: + self.wsjtx_wait = wx.BusyInfo("Starting WSJT-X...") + self.wsjtx_process[0] = subprocess.Popen(prog, shell=False) + else: + self.wsjtx_wait = wx.BusyInfo("WSJT-X was already started") + wx.CallLater(5000, self.StartWsjtxDone, ctrl) + def StartWsjtxDone(self, ctrl): + del self.wsjtx_wait + if ctrl: + method = self.local_conf.globals.get("start_wsjtx", "Never") + ctrl.SetText(method) + #def OnTestTimer(self, event): # temporary code to switch bands and look for a bug + # if event is None: + # self.test_time0 = 0 + # self.test_band = '40' + # self.test_timer = wx.Timer(self) + # self.Bind(wx.EVT_TIMER, self.OnTestTimer) + # self.test_timer.Start(1000, oneShot=True) + # return + # self.bandBtnGroup.SetLabel(self.test_band, do_cmd=True) + # if self.test_band == '40': + # self.test_timer.Start(250, oneShot=True) + # self.test_band = '30' + # else: + # self.test_timer.Start(250, oneShot=True) + # self.test_band = '40' + def OpenHardware(self): + if conf.use_rx_udp and conf.use_rx_udp != 10: + self.add_version = True # Add firmware version to config text + else: + self.add_version = False + if conf.use_rx_udp == 10: # Hermes UDP protocol + if conf.tx_ip == '': + conf.tx_ip = Hardware.hermes_ip + elif conf.tx_ip == 'disable': + conf.tx_ip = '' + if conf.tx_audio_port == 0: + conf.tx_audio_port = conf.rx_udp_port + elif conf.use_rx_udp: + conf.rx_udp_decimation = 8 * 8 * 8 + if conf.tx_ip == '': + conf.tx_ip = conf.rx_udp_ip + elif conf.tx_ip == 'disable': + conf.tx_ip = '' + if conf.tx_audio_port == 0: + conf.tx_audio_port = conf.rx_udp_port + 2 + # Open the hardware. This must be called before open_sound(). + self.config_text = Hardware.open() + if self.config_text: + self.main_frame.SetConfigText(self.config_text) + else: + self.config_text = "Missing config_text" + def MakeSoundDeviceList(self): + # Create the list of capture and play devices (play, prefix, description, device, alsa_device, alsa_description). + # Play is 1 for play, 0 for capture. Prefix is "alsa:", "pulse:", "wasapi:" + self.sound_devices = [] + if sys.platform == 'win32': + dev_capt, dev_play = QS.wasapi_sound_devices() + for description, device, raw in dev_capt: + if raw: + self.sound_devices.append((0, 'wasapi:', description, device, '', '')) + for description, device, raw in dev_play: + if raw: + self.sound_devices.append((1, 'wasapi:', description, device, '', '')) + elif sys.platform == 'darwin': + # Add PortAudio names + dev_capt, dev_play = QS.portaudio_sound_devices() + for name in dev_capt: + self.sound_devices.append((0, 'portaudio:', name, name, '', '')) + for name in dev_play: + self.sound_devices.append((1, 'portaudio:', name, name, '', '')) + else: + # Add Alsa names + dev_capt, dev_play = QS.alsa_sound_devices() + for name in dev_capt: + i = name.rfind(" (hw:") + if i > 5: + device = name[i + 2 : -1] + description = name[0:i] + else: # should not happen + device = name + description = name + self.sound_devices.append((0, 'alsa:', description, device, device, description)) + for name in dev_play: + i = name.rfind(" (hw:") + if i > 5: + device = name[i + 2 : -1] + description = name[0:i] + else: # should not happen + device = name + description = name + self.sound_devices.append((1, 'alsa:', description, device, device, description)) + if QS.get_params("QUISK_HAVE_PULSEAUDIO"): + # Add PulseAudio names + self.sound_devices.append((0, 'pulse:', 'default', 'default', '', '')) + self.sound_devices.append((1, 'pulse:', 'default', 'default', '', '')) + dev_capt, dev_play = QS.pulseaudio_sound_devices() # device, description, alsa description (alsa device) or "" + for device, description, alsa in dev_capt: + i = alsa.rfind(" (hw:") + if i > 5: + alsa_device = alsa[i + 2 : -1] + alsa_description = alsa[0:i] + else: + alsa_device = '' + alsa_description = '' + self.sound_devices.append((0, 'pulse:', description, device, alsa_device, alsa_description)) + for device, description, alsa in dev_play: + i = alsa.rfind(" (hw:") + if i > 5: + alsa_device = alsa[i + 2 : -1] + alsa_description = alsa[0:i] + else: + alsa_device = '' + alsa_description = '' + self.sound_devices.append((1, 'pulse:', description, device, alsa_device, alsa_description)) + if conf.pulse_audio_verbose_output > 2: + print ("Sound devices:") + for play, prefix, description, device, alsa_device, alsa_description in self.sound_devices: + print (prefix, play, description, ';', device, ';', alsa_device, ';', alsa_description, '.') + # Create the list of sound card names + self.dev_capt = [''] + self.dev_play = [''] + pulse_capt = [] + pulse_play = [] + for play, prefix, description, device, alsa_device, alsa_description in self.sound_devices: + if sys.platform == 'win32': + t = description + elif alsa_device: + t = "%s%s (%s)" % (prefix, description, alsa_device) + else: + t = "%s%s" % (prefix, description) + if prefix == "pulse:": + if "QuiskDigital" in t: # Add these names selectively in configure.py + pass + elif play: + pulse_play.append(t) + else: + pulse_capt.append(t) + else: + if play: + self.dev_play.append(t) + else: + self.dev_capt.append(t) + if QS.get_params("QUISK_HAVE_PULSEAUDIO"): + self.dev_capt += pulse_capt + self.dev_play += pulse_play + def SetSoundDevices(self): + # These are parallel to lists in sound.c: (conf name, is_play, index in sound.c) + sound_cards = ( + ("name_of_sound_play", 1, 0), + ("name_of_mic_play", 1, 1), + ("digital_output_name", 1, 2), + ("sample_playback_name", 1, 3), + ("digital_rx1_name", 1, 4), + ("digital_rx2_name", 1, 5), + ("digital_rx3_name", 1, 6), + ("digital_rx4_name", 1, 7), + ("digital_rx5_name", 1, 8), + ("digital_rx6_name", 1, 9), + ("digital_rx7_name", 1, 10), + ("digital_rx8_name", 1, 11), + ("digital_rx9_name", 1, 12), + ("name_of_sound_capt", 0, 0), + ("microphone_name", 0, 1), + ("digital_input_name", 0, 2) + ) + # These must equal the enum in quisk.h + DEV_DRIVER_NONE = 0 + DEV_DRIVER_PORTAUDIO = 1 + DEV_DRIVER_ALSA = 2 + DEV_DRIVER_PULSEAUDIO = 3 + DEV_DRIVER_DIRECTX = 4 + DEV_DRIVER_WASAPI = 5 + DEV_DRIVER_WASAPI2 = 6 + for name, is_play, index in sound_cards: + value = getattr(conf, name) + if not value: + QS.set_sound_name(index, is_play, DEV_DRIVER_NONE, '', '') + continue + if sys.platform == 'win32': + found_desc, found_dev, a_dev, a_desc = self.GetSoundDevice(is_play, value, "wasapi:") + if not found_desc: + found_desc = value + found_dev = value + if conf.use_fast_sound: + if name in ("name_of_sound_play", "name_of_mic_play"): + windows_driver = DEV_DRIVER_WASAPI2 + else: + windows_driver = DEV_DRIVER_WASAPI + else: + windows_driver = DEV_DRIVER_DIRECTX + QS.set_sound_name(index, is_play, windows_driver, found_desc, found_dev) + if conf.pulse_audio_verbose_output > 2: + print (name, "Windows driver %d" % windows_driver, found_desc, ';', found_dev) + continue + if value[0:9] == 'portaudio': + QS.set_sound_name(index, is_play, DEV_DRIVER_PORTAUDIO, value, '') + continue + # strip off ending " (hw:1,0)" + i = value.rfind(" (hw:") + if i > 5: + value = value[0:i] + if value[0:6] == 'pulse:': + # Pulse opens devices based only on the device. We must strip off the "pulse:". + if value[6:].strip() == "default": + found_desc = value + found_dev = "default" + else: + found_desc, found_dev, a_dev, a_desc = self.GetSoundDevice(is_play, value[6:], "pulse:") + if not found_desc: + found_desc = value + found_dev = value[6:].strip() + QS.set_sound_name(index, is_play, DEV_DRIVER_PULSEAUDIO, found_desc, found_dev) + if conf.pulse_audio_verbose_output > 2: + print (name, "Pulse", found_desc, ';', found_dev) + continue + if value[0:5] == 'alsa:': + # Alsa searches for the description if it starts with "alsa:"; else it opens the device name. + found_desc, found_dev, a_dev, a_desc = self.GetSoundDevice(is_play, value[5:], "alsa:") + if not found_desc: + found_desc = value + found_dev = value[5:] + QS.set_sound_name(index, is_play, DEV_DRIVER_ALSA, found_desc, found_dev) + if conf.pulse_audio_verbose_output > 2: + print (name, "Alsa driver %d" % DEV_DRIVER_ALSA, found_desc, ';', found_dev) + continue + # No initial "alsa:", "pulse:", etc. + found_desc, found_dev, a_dev, a_desc = self.GetSoundDevice(is_play, value, "pulse:") # Look for a pulse name + if not found_desc: + a_dev = value + a_desc = value + QS.set_sound_name(index, is_play, DEV_DRIVER_ALSA, a_desc, a_dev) + if conf.pulse_audio_verbose_output > 2: + print (name, "Alsa driver %d" % DEV_DRIVER_ALSA, a_desc, ';', a_dev) + def GetSoundDevice(self, in_play, in_value, in_prefix): + if in_prefix == "pulse:" and in_value[0:10] == " Use name ": # Convert null-sink names "pulse: Use name xxxx" + dev = in_value[10:] + if dev[-8:] == ".monitor": + dev = dev[0:-8] + else: + dev = dev + ".monitor" + found = [in_value[1:], dev, '', ''] + return found + found = None + partial = None + for data in self.sound_devices: + play, prefix, description, device, alsa_device, alsa_description = data + if play != in_play: + continue + if prefix != in_prefix: + continue + if in_value == description or in_value == device: + found = data + break + if in_value in description: + partial = data + if found: + return found[2:] # description, device, alsa_device, alsa_description + elif partial: + return partial[2:] + else: + return None, None, None, None + def OpenSound(self): + self.MakeSoundDeviceList() + if hasattr(conf, 'mixer_settings'): + for dev, numid, value in conf.mixer_settings: + err_msg = QS.mixer_set(dev, numid, value) + if err_msg: + print("Mixer", err_msg) + QS.capt_channels (conf.channel_i, conf.channel_q) + QS.play_channels (conf.channel_i, conf.channel_q) + QS.micplay_channels (conf.mic_play_chan_I, conf.mic_play_chan_Q) + # Note: Subsequent calls to set channels must not name a higher channel number. + # Normally, these calls are only used to reverse the channels. + self.SetSoundDevices() + QS.open_sound(0, conf.data_poll_usec, conf.latency_millisecs, + conf.tx_ip, conf.tx_audio_port, + conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q, + 0.7, conf.mic_playback_rate) + def OnDxClChange(self): + self.station_screen.Refresh() + def OnIdle(self, event): + if self.screen: + self.screen.OnIdle(event) + def OnEndSession(self, event): + event.Skip() + self.OnBtnClose(event) + def OnBtnClose(self, event=None): + msg = QS.GetQuiskPrintf() + if msg: + print(msg, end='') + QS.close_key() + QS.set_file_name(record_button=0) # Turn off file recording + time.sleep(0.1) + if self.sound_thread: + if self.samples_from_python: + Hardware.StopSamples() + self.sound_thread.stop() + for i in range(0, 20): + if not self.sound_thread.is_alive(): + break + time.sleep(0.1) + self.sound_thread = None + QS.close_rx_udp() + Hardware.close() + self.SaveState() + self.local_conf.SaveState() + if self.hamlib_socket: + self.hamlib_socket.close() + self.hamlib_socket = None + if self.dxCluster: + self.dxCluster.stop() + if self.hamlib_com1_handler: + self.hamlib_com1_handler.close() + if self.hamlib_com2_handler: + self.hamlib_com2_handler.close() + if conf.add_freedv_button: + QS.freedv_close() + msg = QS.GetQuiskPrintf() + if msg: + print(msg, end='') + def OnBtnOnOff(self, event): + if event.GetEventObject().GetValue(): # Start samples + try: + wx.BeginBusyCursor() + time.sleep(2.0) # delay needed to release sound devices + self.OnInit() + finally: + wx.EndBusyCursor() + else: # Stop samples + try: + wx.BeginBusyCursor() + self.OnBtnClose() + time.sleep(2.0) # delay needed to release sound devices + finally: + wx.EndBusyCursor() + def FixAmplPhase(self): + # Convert from [[VFO, freq, ampl, phase], ...] to [[VFO, [[freq, ampl, phase], ...]], ...] + self.bandAmplPhase["Version"] = {'rx':[]} # marker for new format + for band in self.bandAmplPhase: + if not isinstance(self.bandAmplPhase[band], dict): + self.bandAmplPhase[band] = {} + try: + for rx_tx in self.bandAmplPhase[band]: + if rx_tx in ('rx', 'tx'): + data = self.bandAmplPhase[band][rx_tx] + if data and not isinstance(data[0][1], (list, tuple)): + lst = [] + for vfo, freq, am, ph in data: + lst.append([vfo, [[freq, am, ph]]]) + self.bandAmplPhase[band][rx_tx] = lst + except: + traceback.print_exc() + self.bandAmplPhase[band] = {} + def ImmediateChange(self, name): + if name == "pulse_audio_verbose_output": + value = getattr(conf, name) + if value == 0: + self.std_out_err.Show(False) + else: + self.std_out_err.Show(True) + elif name == "quisk_serial_port": + QS.open_key(port=conf.quisk_serial_port) + elif name == "quisk_serial_cts": + QS.open_key(cts=conf.quisk_serial_cts) + elif name == "quisk_serial_dsr": + QS.open_key(dsr=conf.quisk_serial_dsr) + if hasattr(Hardware, "ImmediateChange"): + Hardware.ImmediateChange(name) + QS.ImmediateChange(name) + def CheckState(self): # check whether state has changed + changed = False + for n in self.StateNames: + try: + if getattr(self, n) != self.savedState[n]: + changed = True + break + except: + changed = True + break + return changed + def SaveState(self): + state = {} + for n in self.StateNames: + state[n] = v = getattr(self, n) + self.savedState[n] = v + path = os.path.join(self.QuiskFilesDir, 'quisk_init.json') + try: + fp = open(path, "w") + json.dump(state, fp, indent=2) + fp.close() + except: + pass #traceback.print_exc() + def Mode2Filters(self, mode): # return the list of filter bandwidths for each mode + if mode in ('CWL', 'CWU'): + return conf.FilterBwCW + if mode in ('LSB', 'USB'): + return conf.FilterBwSSB + if mode == 'AM': + return conf.FilterBwAM + if mode in ('FM', 'DGT-FM', 'DGT-IQ'): + return conf.FilterBwFM + if mode in ('DGT-U', 'DGT-L'): + return conf.FilterBwDGT + if mode[0:4] == 'FDV-': + return conf.FilterBwFDV + if mode == 'IMD': + return conf.FilterBwIMD + if mode == 'EXT': + return conf.FilterBwEXT + return conf.FilterBwSSB + def OnSmeterRightDown(self, event): + try: + pos = event.GetPosition() # works for right-click + self.smeter.TextCtrl.PopupMenu(self.smeter_menu, pos) + except: + btn = event.GetEventObject() # works for button + btn.PopupMenu(self.smeter_menu, (0,0)) + def OnSmeterMeterA(self, event): + self.smeter_avg_seconds = 1.0 + self.smeter_usage = "smeter" + QS.measure_frequency(0) + def OnSmeterMeterB(self, event): + self.smeter_avg_seconds = 5.0 + self.smeter_usage = "smeter" + QS.measure_frequency(0) + def OnSmeterFrequencyA(self, event): + self.smeter_usage = "freq" + QS.measure_frequency(2) + def OnSmeterFrequencyB(self, event): + self.smeter_usage = "freq" + QS.measure_frequency(10) + def OnSmeterAudioA(self, event): + self.smeter_usage = "audio" + QS.measure_audio(1) + def OnSmeterAudioB(self, event): + self.smeter_usage = "audio" + QS.measure_audio(5) + def QuiskNewId(self): + try: + ref = wx.NewIdRef() + self.NewIdList.append(ref) + rid = ref.GetValue() + except AttributeError: + rid = wx.NewId() + return rid + def MakeAccel(self, button): + rid = self.QuiskNewId() + self.main_frame.Bind(wx.EVT_MENU, button.Shortcut, id=rid) + self.accel_list.append(wx.AcceleratorEntry(wx.ACCEL_ALT, ord(button.char_shortcut), rid)) + def MakeButtons(self, frame, gbs): + from quisk_widgets import button_text_width + margin = button_text_width + # Make one or two sliders on the left + self.sliderVol = SliderBoxV(frame, 'Vol', self.volumeAudio, 1000, self.ChangeVolume) + self.ChangeVolume() # set initial volume level + if conf.use_sidetone: + self.sliderSto = SliderBoxV(frame, 'STo', self.sidetone_volume, 1000, self.ChangeSidetone) + self.ChangeSidetone() + else: + self.sliderSto = None + # Make four sliders on the right + self.ritScale = SliderBoxV(frame, 'Rit', self.ritFreq, 2000, self.OnRitScale, False, themin=-2000) + self.sliderYs = SliderBoxV(frame, 'Ys', 0, 160, self.ChangeYscale, True) + self.sliderYz = SliderBoxV(frame, 'Yz', 0, 160, self.ChangeYzero, True) + self.sliderZo = SliderBoxV(frame, 'Zo', 0, 1000, self.OnChangeZoom) + self.sliderZo.SetValue(0) + flag = wx.EXPAND + # Add band buttons + if conf.button_layout == 'Large screen': + self.widget_row = 4 # Next available row for widgets + shortcuts = [] + for label in conf.bandLabels: + if isinstance(label, (list, tuple)): + label = label[0] + shortcuts.append(conf.bandShortcuts.get(label, '')) + self.bandBtnGroup = RadioButtonGroup(frame, self.OnBtnBand, conf.bandLabels, None, shortcuts) + else: + self.widget_row = 6 # Next available row for widgets + self.bandBtnGroup = RadioBtnPopup(frame, self.OnBtnBand, conf.bandLabels, None, 'bandBtnGroup') + self.bandBtnGroup.idName = 'bandBtnGroup' + # Add sliders on the left + gbs.Add(self.sliderVol, (0, 0), (self.widget_row, 1), flag=wx.EXPAND|wx.LEFT, border=margin) + if conf.use_sidetone: + button_start_col = 2 + self.slider_columns = [0, 1] + gbs.Add(self.sliderSto, (0, 1), (self.widget_row, 1), flag=flag) + else: + self.slider_columns = [0] + button_start_col = 1 + # Receive button row: Mute, NR2, AGC + left_row2 = [] + b = b_mute = QuiskCheckbutton(frame, self.OnBtnMute, text='Mute') + b.char_shortcut = 'u' + self.MakeAccel(b) + left_row2.append(b) + b = QuiskCheckbutton(frame, self.OnBtnNR2, text='NR2') + if not self.wdsp.version: + b.Enable(False) + left_row2.append(b) + self.BtnAGC = agc = QuiskCheckbutton(frame, self.OnBtnAGC, 'AGC') + agc.char_shortcut = 'G' + self.MakeAccel(agc) + b = self.SliderAGC = WrapSlider(agc, self.OnBtnAGC, display=True) + self.midiControls["AGCSlider"] = (b, None) + b.SetDual(True) + b.SetSlider(value_off=self.levelOffAGC, value_on=self.levelAGC) + agc.SetValue(True, True) + left_row2.append(b) + b = self.BtnSquelch = QuiskCheckbutton(frame, self.OnBtnSquelch, text='Sqlch') + b.char_shortcut = 'q' + self.MakeAccel(b) + self.sliderSquelch = WrapSlider(b, self.OnBtnSquelch, display=True) + self.midiControls["SqlchSlider"] = (self.sliderSquelch, None) + left_row2.append(self.sliderSquelch) + # Noise Blanker + self.NB_menu = QuiskMenu("NB_menu") + for t in ("NB 1", "NB 2", "NB 3"): + self.NB_menu.AppendRadioItem(t, self.OnMenuNB, t == "NB 1") + item = self.NB_menu.AppendRadioItem("SNB", self.OnMenuNB) + if not self.wdsp.version: + item.Enable(False) + self.btnNB = QuiskCheckbutton(frame, self.OnBtnNB, text='NB 1') + self.btnNB.char_shortcut = 'B' + self.MakeAccel(self.btnNB) + b = WrapMenu(self.btnNB, self.NB_menu) + left_row2.append(b) + # Notch + b = QuiskCheckbutton(frame, self.OnBtnAutoNotch, text='Notch') + b.char_shortcut = 'h' + self.MakeAccel(b) + left_row2.append(b) + try: + gain_labels = Hardware.rf_gain_labels + except: + gain_labels = () + try: + ant_labels = Hardware.antenna_labels + except: + ant_labels = () + self.BtnRfGain = None + add_2 = 0 # Add two more buttons + if gain_labels: + b = self.BtnRfGain = QuiskCycleCheckbutton(frame, Hardware.OnButtonRfGain, gain_labels) + left_row2.append(b) + add_2 += 1 + if ant_labels: + b = QuiskCycleCheckbutton(frame, Hardware.OnButtonAntenna, ant_labels) + left_row2.append(b) + add_2 += 1 + if add_2 == 0: + b = QuiskCheckbutton(frame, None, text='RfGain') + b.Enable(False) + left_row2.append(b) + add_2 += 1 + if add_2 == 1: + if 0: # Display a color chooser + #b_test1 = QuiskPushbutton(frame, self.OnBtnColorDialog, 'Color') + b_test1 = QuiskRepeatbutton(frame, self.OnBtnColor, 'Color', use_right=True) + else: + b_test1 = self.test1Button = QuiskCheckbutton(frame, self.OnBtnTest1, 'Test 1', color=conf.color_test) + left_row2.append(b_test1) + else: + b_test1 = None + # Transmit button row: Spot + left_row3=[] + bt = self.spotButton = QuiskCheckbutton(frame, self.OnBtnSpot, 'Spot', color=conf.color_test) + b = WrapSlider(bt, self.OnBtnSpot, slider_value=self.levelSpot, display=True) + self.midiControls["SpotSlider"] = (b, None) + if hasattr(Hardware, 'OnSpot'): + bt.char_shortcut = 'o' + self.MakeAccel(bt) + else: + b.Enable(False) + left_row3.append(b) + # Split button + self.split_menu = QuiskMenu("split_menu") + pl = self.split_rxtx_play + self.split_menu.AppendRadioItem('Play both, High Freq on R', self.OnMenuSplitPlay1, pl == 1) + self.split_menu.AppendRadioItem('Play both, High Freq on L', self.OnMenuSplitPlay2, pl == 2) + self.split_menu.AppendRadioItem('Play only Rx', self.OnMenuSplitPlay3, pl == 3) + self.split_menu.AppendRadioItem('Play only Tx', self.OnMenuSplitPlay4, pl == 4) + self.split_menu.AppendSeparator() + self.split_menu.AppendRadioItem('Unlocked', self.OnMenuSplitLock, True) + self.split_menu.AppendRadioItem('Lock Tx, Split Rx', self.OnMenuSplitLock, False) + self.split_menu.AppendRadioItem('Lock Rx, Split Tx', self.OnMenuSplitLock, False) + self.split_menu.AppendSeparator() + self.split_menu.Append('Reverse Rx and Tx', self.OnMenuSplitRev) + b = QuiskCheckbutton(frame, self.OnBtnSplit, "Split") + b.char_shortcut = 'l' + self.MakeAccel(b) + self.splitButton = WrapMenu(b, self.split_menu) + if conf.mouse_tune_method: # Mouse motion changes the VFO frequency + self.splitButton.Enable(False) + left_row3.append(self.splitButton) + b = QuiskCheckbutton(frame, self.OnBtnFDX, 'FDX', color=conf.color_test) + if conf.add_fdx_button: + b.char_shortcut = 'X' + self.MakeAccel(b) + else: + b.Enable(False) + left_row3.append(b) + bt = QuiskCheckbutton(frame, self.OnButtonPTT, 'PTT', color='red') + self.pttButton = bt + b = WrapIndicator(bt, "Tx", "white", 'red') + self.pttButton.Tx = b + left_row3.append(b) + b = QuiskCheckbutton(frame, self.OnButtonVOX, 'VOX') + b.char_shortcut = 'V' + self.MakeAccel(b) + left_row3.append(b) + # add another receiver + self.multi_rx_menu = wx.Menu() + item = self.multi_rx_menu.AppendRadioItem(-1, 'Play only') + self.Bind(wx.EVT_MENU, self.OnMultirxPlayBoth, item) + item = self.multi_rx_menu.AppendRadioItem(-1, 'Play on left') + self.Bind(wx.EVT_MENU, self.OnMultirxPlayLeft, item) + item = self.multi_rx_menu.AppendRadioItem(-1, 'Play on right') + self.Bind(wx.EVT_MENU, self.OnMultirxPlayRight, item) + btn_addrx = QuiskPushbutton(frame, self.multi_rx_screen.OnAddReceiver, "Add Rx") + btn_addrx = WrapMenu(btn_addrx, self.multi_rx_menu) + if not hasattr(Hardware, 'MultiRxCount'): + btn_addrx.Enable(False) + # Record and Playback buttons + b = self.btnTmpRecord = QuiskCheckbutton(frame, self.OnBtnTmpRecord, text=conf.Xbtn_text_rec) + #left_row3.append(b) + b = self.btnTmpPlay = QuiskCheckbutton(frame, self.OnBtnTmpPlay, text=conf.Xbtn_text_play, use_right=True) + b.Enable(0) + #left_row3.append(b) + self.btn_file_record = QuiskCheckbutton(frame, self.OnBtnFileRecord, conf.Xbtn_text_file_rec) + left_row3.append(self.btn_file_record) + self.btnFilePlay = QuiskCheckbutton(frame, self.OnBtnFilePlay, conf.Xbtn_text_file_play) + left_row3.append(self.btnFilePlay) + self.config_screen.config.InitRecPlay() + self.config_screen.config.EnableRecPlay() + ### Right bank of buttons + mode_names = ['CWL', 'CWU', 'LSB', 'USB', 'AM', 'FM', 'DGT-U', 'DGT-L', 'DGT-FM', 'DGT-IQ', 'FDV-U', 'FDV-L', 'IMD'] + labels = [('CWL', 'CWU'), ('LSB', 'USB'), 'AM', 'FM', ('DGT-U', 'DGT-L', 'DGT-FM', 'DGT-IQ')] + shortcuts = ['C', 'S', 'A', 'M', 'D'] + count = 5 # There is room for seven buttons + if conf.add_freedv_button: + n_freedv = count + count += 1 + labels.append('FDV-U') + shortcuts.append('F') + if conf.add_imd_button: + n_imd = count + count += 1 + labels.append('IMD') + shortcuts.append('I') + if count < 7 and conf.add_extern_demod: + count += 1 + labels.append(conf.add_extern_demod) + mode_names.append(conf.add_extern_demod) + shortcuts.append('') + while count < 7: + count += 1 + labels.append('') + shortcuts.append('') + mode_names.sort() + self.config_screen.favorites.SetModeEditor(mode_names) + if conf.button_layout == 'Large screen': + self.modeButns = RadioButtonGroup(frame, self.OnBtnMode, labels, None, shortcuts) + self.modeButns.buttons[0].idName = "CW U/L" + self.modeButns.buttons[1].idName = "SSB U/L" + self.modeButns.buttons[4].idName = "DGT" + else: + labels = ['CWL', 'CWU', 'LSB', 'USB', 'AM', 'FM', 'DGT-U', 'DGT-L', 'DGT-FM', 'DGT-IQ', 'FDV-U', 'IMD'] + self.modeButns = RadioBtnPopup(frame, self.OnBtnMode, labels, None, 'modeButns') + self.modeButns.idName = "modeButns" + self.freedv_menu_items = {} + if conf.add_freedv_button: + self.freedv_menu = QuiskMenu("freedv_menu") + msg = conf.freedv_tx_msg + QS.freedv_set_options(mode=conf.freedv_modes[0][1], tx_msg=msg, DEBUG=0, squelch=1) + for mode, index in conf.freedv_modes: + item = self.freedv_menu.AppendRadioItem(mode, self.OnFreedvMenu, mode == self.freedv_mode) + self.freedv_menu_items[index] = item + if mode == self.freedv_mode: # Restore mode + QS.freedv_set_options(mode=index) + if conf.button_layout == 'Large screen': + b = QuiskCycleCheckbutton(frame, None, ('FDV-U', 'FDV-L'), is_radio=True) + b.idName = "FDV" + b.char_shortcut = 'F' + self.btnFreeDV = WrapMenu(b, self.freedv_menu) + self.modeButns.ReplaceButton(n_freedv, self.btnFreeDV) + else: + self.btnFreeDV = self.modeButns.AddMenu('FDV-U', self.freedv_menu) + try: + ok = QS.freedv_open() + except: + traceback.print_exc() + ok = 0 + if not ok: + conf.add_freedv_button = False + if conf.button_layout == 'Large screen': + self.modeButns.GetButtons()[n_freedv].Enable(0) + else: + self.modeButns.Enable('FDV-U', False) + if conf.add_imd_button: + val = 500 + QS.set_imd_level(val) + if conf.button_layout == 'Large screen': + b = QuiskCheckbutton(frame, None, 'IMD', color=conf.color_test) + b.char_shortcut = 'I' + b = WrapSlider(b, self.OnImdSlider, slider_value=val, display=True) + self.midiControls["IMDSlider"] = (b, None) + self.modeButns.ReplaceButton(n_imd, b) + else: + self.modeButns.AddSlider('IMD', self.OnImdSlider, slider_value=val, display=True) + labels = ('2000', '2000', '2000', '2000', '2000', '2000') + self.filterButns = RadioButtonGroup(frame, self.OnBtnFilter, labels, None) + self.filterButns.idName = "filterButns" + b = QuiskCheckbutton(frame, None, str(self.filterAdjBw1)) + b = WrapSlider(b, self.OnBtnAdjFilter, slider_value=self.filterAdjBw1, wintype='filter') + self.midiControls["Filter 6Slider"] = (b, None) + self.filterButns.ReplaceButton(5, b) + right_row2 = self.filterButns.GetButtons() + for i in range(0, 6): + b = right_row2[i] + if isinstance(b, WrapSlider): + b.button.idName = "Filter %d" % (i + 1) + #b.idName = "Filter" + else: + b.idName = "Filter %d" % (i + 1) + if self.rate_audio_fft: + t = "Audio FFT" + elif self.bandscope_clock: # Hermes UDP protocol + t = "Bscope" + else: + t = "RX Filter" + if conf.button_layout == 'Large screen': + labels = (('Graph', 'GraphP1', 'GraphP2'), ('WFall', 'WFallP1', 'WFallP2'), ('Scope', 'Scope'), 'Config', t, 'Help') + self.screenBtnGroup = RadioButtonGroup(frame, self.OnBtnScreen, labels, conf.default_screen) + right_row3 = self.screenBtnGroup.GetButtons() + else: + labels = ('Graph', 'GraphP1', 'GraphP2', 'WFall', 'WFallP1', 'WFallP2', 'Scope', 'Config', t) + self.screenBtnGroup = RadioBtnPopup(frame, self.OnBtnScreen, labels, conf.default_screen, 'screenBtnGroup') + for button in self.screenBtnGroup.GetButtons(): + self.idName2Button[button.idName] = self.screenBtnGroup + self.screenBtnGroup.idName = "screenBtnGroup" + # Top row ----------------- + # Band down button + szr = wx.BoxSizer(wx.HORIZONTAL) # add control to box sizer for centering + b_bandupdown = szr + b = QuiskRepeatbutton(frame, self.OnBtnDownBand, conf.Xbtn_text_range_dn, + self.OnBtnUpDnBandDone, use_right=True) + b.idName = "Band " + b.idName + self.idName2Button[b.idName] = b + szr.Add(b, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=1) + # Band up button + b = QuiskRepeatbutton(frame, self.OnBtnUpBand, conf.Xbtn_text_range_up, + self.OnBtnUpDnBandDone, use_right=True) + b.idName = "Band " + b.idName + self.idName2Button[b.idName] = b + szr.Add(b, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=1) + # Memory buttons + szr = wx.BoxSizer(wx.HORIZONTAL) # add control to box sizer for centering + b_membtn = szr + b = QuiskPushbutton(frame, self.OnBtnMemSave, conf.Xbtn_text_mem_add) + szr.Add(b, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=1) + b = self.memNextButton = QuiskPushbutton(frame, self.OnBtnMemNext, conf.Xbtn_text_mem_next) + b.Enable(False) + self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightClickMemory, b) + szr.Add(b, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT|wx.RIGHT, border=1) + b = self.memDeleteButton = QuiskPushbutton(frame, self.OnBtnMemDelete, conf.Xbtn_text_mem_del) + b.Enable(False) + szr.Add(b, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=1) + # Favorites buttons + szr = wx.BoxSizer(wx.HORIZONTAL) # add control to box sizer for centering + b_fav = szr + b = self.StationNewButton = QuiskPushbutton(frame, self.OnBtnFavoritesNew, conf.Xbtn_text_fav_add) + szr.Add(b, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=1) + b = self.StationNewButton = QuiskPushbutton(frame, self.OnBtnFavoritesShow, conf.Xbtn_text_fav_recall) + szr.Add(b, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=1) + # Add another receiver + szr = wx.BoxSizer(wx.HORIZONTAL) # add control to box sizer for centering + b_addrx = szr + szr.Add(btn_addrx, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=1) + # Temporary play and record + szr = wx.BoxSizer(wx.HORIZONTAL) # add control to box sizer for centering + b_tmprec = szr + szr.Add(self.btnTmpRecord, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=1) + szr.Add(self.btnTmpPlay, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=1) + # RIT button + szr = wx.BoxSizer(wx.HORIZONTAL) # add control to box sizer for centering + b_rit = szr + self.ritButton = QuiskCheckbutton(frame, self.OnBtnRit, "RIT") + self.idName2Button[self.ritButton.idName] = self.ritButton + szr.Add(self.ritButton, 1, flag=wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, border=1) + self.ritButton.SetLabel("RIT %d" % self.ritFreq) + self.ritButton.Refresh() + # Frequency display + bw, bh = b_mute.GetMinSize() + b_freqdisp = self.freqDisplay = FrequencyDisplay(frame, 99999, bh * 15 // 10) + self.freqDisplay.Display(self.txFreq + self.VFO) + # On/Off button + if conf.button_layout == 'Large screen': + b_onoff = QuiskCheckbutton(frame, self.OnBtnOnOff, "On", color='#77DD77') + b_onoff.SetValue(True, do_cmd=False) + h = b_freqdisp.height + b_onoff.SetSizeHints(h, h, h, h) + # Frequency entry + if conf.button_layout == 'Large screen': + e = wx.TextCtrl(frame, -1, '', size=(10, bh), style=wx.TE_PROCESS_ENTER) + font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + e.SetFont(font) + e.SetBackgroundColour(conf.color_entry) + e.SetForegroundColour(conf.color_entry_txt) + szr = wx.BoxSizer(wx.HORIZONTAL) # add control to box sizer for centering + b_freqenter = szr + szr.Add(e, 1, flag=wx.ALIGN_CENTER_VERTICAL) + frame.Bind(wx.EVT_TEXT_ENTER, self.FreqEntry, source=e) + # S-meter + self.smeter = QuiskText(frame, ' S9+23 -166.00 dB ', bh, 0, True) + from quisk_widgets import _bitmap_menupop + b = QuiskBitmapButton(frame, self.OnSmeterRightDown, _bitmap_menupop) + szr = wx.BoxSizer(wx.HORIZONTAL) + b_smeter = szr + szr.Add(self.smeter, 1, flag=wx.ALIGN_CENTER_VERTICAL) + szr.Add(b, 0, flag=wx.ALIGN_CENTER_VERTICAL) + self.smeter.TextCtrl.Bind(wx.EVT_RIGHT_DOWN, self.OnSmeterRightDown) + self.smeter.TextCtrl.SetBackgroundColour(conf.color_freq) + self.smeter.TextCtrl.SetForegroundColour(conf.color_freq_txt) + # Make a popup menu for the s-meter + self.smeter_menu = QuiskMenu("smeter_menu") + self.smeter_menu.AppendRadioItem('S-meter 1', self.OnSmeterMeterA) + self.smeter_menu.AppendRadioItem('S-meter 5', self.OnSmeterMeterB) + self.smeter_menu.AppendRadioItem('Frequency 2', self.OnSmeterFrequencyA) + self.smeter_menu.AppendRadioItem('Frequency 10', self.OnSmeterFrequencyB) + self.smeter_menu.AppendRadioItem('Audio 1', self.OnSmeterAudioA) + self.smeter_menu.AppendRadioItem('Audio 5', self.OnSmeterAudioB) + # Make a popup menu for the memory buttons + self.memory_menu = wx.Menu() + # Place the buttons on the screen + if conf.button_layout == 'Large screen': + # There are fourteen columns, a small gap column, and then twelve more columns + band_buttons = self.bandBtnGroup.buttons + if len(band_buttons) <= 7: + bmax = 7 + span = 2 + else: + bmax = 14 + span = 1 + col = 0 + for b in band_buttons[0:bmax]: + self.idName2Button[b.idName] = b + gbs.Add(b, (1, button_start_col + col), (1, span), flag=flag) + col += span + while col < 14: + b = QuiskCheckbutton(frame, None, text='') + gbs.Add(b, (1, button_start_col + col), (1, span), flag=flag) + col += span + col = button_start_col + for b in left_row2: + self.idName2Button[b.idName] = b + if b.idName in ("Mute", "NR2"): + gbs.Add(b, (2, col), (1, 1), flag=flag) + col += 1 + else: + gbs.Add(b, (2, col), (1, 2), flag=flag) + col += 2 + col = button_start_col + for b in left_row3: + self.idName2Button[b.idName] = b + gbs.Add(b, (3, col), (1, 2), flag=flag) + col += 2 + col = 15 + for b in self.modeButns.GetButtons(): + self.idName2Button[b.idName] = b + if col in (19, 20): # single column + gbs.Add(b, (1, button_start_col + col), flag=flag) + col += 1 + else: # double column + gbs.Add(b, (1, button_start_col + col), (1, 2), flag=flag) + col += 2 + col = button_start_col + 15 + for i in range(0, 6): + b = right_row2[i] + gbs.Add(b, (2, col), (1, 2), flag=flag) + self.idName2Button[b.idName] = b + b = right_row3[i] + gbs.Add(b, (3, col), (1, 2), flag=flag) + self.idName2Button[b.idName] = b + col += 2 + gbs.Add(b_onoff, (0, button_start_col), (1, 1), + flag=wx.EXPAND | wx.TOP | wx.BOTTOM, border=self.freqDisplay.border) + self.idName2Button[b_onoff.idName] = b_onoff + gbs.Add(b_freqdisp, (0, button_start_col + 1), (1, 5), + flag=wx.EXPAND | wx.TOP | wx.BOTTOM, border=self.freqDisplay.border) + gbs.Add(b_freqenter, (0, button_start_col + 6), (1, 2), flag = wx.EXPAND|wx.LEFT|wx.RIGHT, border=5) + gbs.Add(b_bandupdown, (0, button_start_col + 8), (1, 2), flag=wx.EXPAND) + gbs.Add(b_membtn, (0, button_start_col + 11), (1, 3), flag = wx.EXPAND) + gbs.Add(b_fav, (0, button_start_col + 15), (1, 2), flag=wx.EXPAND) + gbs.Add(b_tmprec, (0, button_start_col + 17), (1, 2), flag=wx.EXPAND) + gbs.Add(b_addrx, (0, button_start_col + 19), (1, 2), flag=wx.EXPAND) + gbs.Add(b_smeter, (0, button_start_col + 21), (1, 4), flag=wx.EXPAND) + gbs.Add(b_rit, (0, button_start_col + 25), (1, 2), flag=wx.EXPAND) + col = button_start_col + 28 + self.slider_columns += [col, col + 1, col + 2, col + 3] + gbs.Add(self.ritScale, (0, col ), (self.widget_row, 1), flag=wx.EXPAND|wx.LEFT, border=margin) + gbs.Add(self.sliderYs, (0, col + 1), (self.widget_row, 1), flag=flag) + gbs.Add(self.sliderYz, (0, col + 2), (self.widget_row, 1), flag=flag) + gbs.Add(self.sliderZo, (0, col + 3), (self.widget_row, 1), flag=flag) + for i in range(button_start_col, button_start_col + 14): + gbs.AddGrowableCol(i,1) + for i in range(button_start_col + 15, button_start_col + 27): + gbs.AddGrowableCol(i,1) + else: # Small screen + gbs.Add(b_freqdisp, (0, button_start_col), (1, 6), + flag=wx.EXPAND | wx.TOP | wx.BOTTOM, border=self.freqDisplay.border) + gbs.Add(b_bandupdown, (0, button_start_col + 6), (1, 2), flag=wx.EXPAND) + gbs.Add(b_smeter, (0, button_start_col + 8), (1, 4), flag=wx.EXPAND) + + gbs.Add(self.bandBtnGroup.GetPopControl(), (1, button_start_col), (1, 2), flag=flag) + gbs.Add(self.modeButns.GetPopControl(), (3, button_start_col), (1, 2), flag=flag) + gbs.Add(self.screenBtnGroup.GetPopControl(), (4, button_start_col), (1, 2), flag=flag) + b = QuiskCheckbutton(frame, self.OnBtnHelp, 'Help') + gbs.Add(b, (5, button_start_col), (1, 2), flag=flag) + + gbs.Add(b_membtn, (1, button_start_col + 2), (1, 3), flag = wx.EXPAND) + gbs.Add(b_fav, (1, button_start_col + 5), (1, 2), flag = wx.EXPAND) + gbs.Add(b_tmprec, (1, button_start_col + 7), (1, 2), flag=wx.EXPAND) + b = QuiskPushbutton(frame, None, '') + gbs.Add(b, (1, button_start_col + 9), (1, 1), flag=wx.EXPAND) + gbs.Add(b_rit, (1, button_start_col + 10), (1, 2), flag=wx.EXPAND) + + row = 2 + col = button_start_col + for b in self.filterButns.GetButtons(): + self.idName2Button[b.idName] = b + gbs.Add(b, (row, col), (1, 2), flag=flag) + col += 2 + + buttons = left_row2 + left_row3 + if b_test1: + buttons.remove(b_test1) + buttons += [b_test1, b_addrx] + else: + buttons += [b_addrx] + row = 3 + col = 2 + for b in buttons: + if hasattr(b, "idName"): + self.idName2Button[b.idName] = b + if hasattr(b, "idName") and b.idName in ("Mute", "NR2"): + gbs.Add(b, (row, button_start_col + col), (1, 1), flag=flag) + col += 1 + else: + gbs.Add(b, (row, button_start_col + col), (1, 2), flag=flag) + col += 2 + if col >= 12: + row += 1 + col = 2 + col = button_start_col + 12 + self.slider_columns += [col, col + 1, col + 2, col + 3] + gbs.Add(self.ritScale, (0, col), (self.widget_row, 1), flag=wx.EXPAND|wx.LEFT, border=margin) + gbs.Add(self.sliderYs, (0, col + 1), (self.widget_row, 1), flag=flag) + gbs.Add(self.sliderYz, (0, col + 2), (self.widget_row, 1), flag=flag) + gbs.Add(self.sliderZo, (0, col + 3), (self.widget_row, 1), flag=flag) + for i in range(button_start_col, button_start_col + 12): + gbs.AddGrowableCol(i,1) + self.button_start_col = button_start_col + def MeasureAudioVoltage(self): + v = QS.measure_audio(-1) + t = "%11.3f" % v + t = t[0:1] + ' ' + t[1:4] + ' ' + t[4:] + ' uV' + self.smeter.SetLabel(t) + def MeasureFrequency(self): + vfo = Hardware.ReturnVfoFloat() + if vfo is None: + vfo = self.VFO + vfo += Hardware.transverter_offset + t = '%13.2f' % (QS.measure_frequency(-1) + vfo) + t = t[0:4] + ' ' + t[4:7] + ' ' + t[7:] + ' Hz' + self.smeter.SetLabel(t) + def NewDVmeter(self): + if conf.add_freedv_button: + snr = QS.freedv_get_snr() + txt = QS.freedv_get_rx_char() + self.graph.ScrollMsg(txt) + self.waterfall.ScrollMsg(txt) + else: + snr = 0.0 + t = " SNR %3.0f" % snr + self.smeter.SetLabel(t) + def NewSmeter(self): + self.smeter_db_count += 1 # count for average + x = QS.get_smeter() + self.smeter_db_sum += x # sum for average + if self.timer - self.smeter_db_time0 > self.smeter_avg_seconds: # average time reached + self.smeter_db = self.smeter_db_sum / self.smeter_db_count + self.smeter_db_count = self.smeter_db_sum = 0 + self.smeter_db_time0 = self.timer + if self.smeter_sunits < x: # S-meter moves to peak value + self.smeter_sunits = x + else: # S-meter decays at this time constant + self.smeter_sunits -= (self.smeter_sunits - x) * (self.timer - self.smeter_sunits_time0) + self.smeter_sunits_time0 = self.timer + s = self.smeter_sunits / 6.0 # change to S units; 6db per S unit + s += Hardware.correct_smeter # S-meter correction for the gain, band, etc. + if s < 0: + s = 0 + if s >= 9.5: + s = (s - 9.0) * 6 + t = " S9+%2.0f %7.2f dB" % (s, self.smeter_db) + else: + t = " S%.0f %7.2f dB" % (s, self.smeter_db) + self.smeter.SetLabel(t) + def MakeFilterButtons(self, args): + # Change the filter selections depending on the mode: CW, SSB, etc. + # Do not change the adjustable filter buttons. + buttons = self.filterButns.GetButtons() + for i in range(0, len(buttons) - 1): + label = str(args[i]) + buttons[i].SetLabel(label) + buttons[i].Refresh() + if label: + buttons[i].Enable(1) + else: + buttons[i].Enable(0) + def MakeFilterCoef(self, rate, N, bw, center): + """Make an I/Q filter with rectangular passband.""" + center = abs(center) + lowpass = bw * 24000 // rate // 2 + if lowpass in Filters: + filtD = Filters[lowpass] + #print ("Custom filter key %d rate %d bandwidth %d size %d" % (lowpass, rate, bw, len(filtD))) + else: + #print ("Window filter key %d rate %d bandwidth %d" % (lowpass, rate, bw)) + if N is None: + shape = 1.5 # Shape factor at 88 dB + trans = (bw / 2.0 / rate) * (shape - 1.0) # 88 dB atten + N = int(4.0 / trans) + if N > 1000: + N = 1000 + N = (N // 2) * 2 + 1 + K = bw * N // rate + filtD = [] + pi = math.pi + sin = math.sin + cos = math.cos + for k in range(-N//2, N//2 + 1): + # Make a lowpass filter + if k == 0: + z = float(K) / N + else: + z = 1.0 / N * sin(pi * k * K / N) / sin(pi * k / N) + # Apply a windowing function + if 1: # Blackman window + w = 0.42 + 0.5 * cos(2. * pi * k / N) + 0.08 * cos(4. * pi * k / N) + elif 0: # Hamming + w = 0.54 + 0.46 * cos(2. * pi * k / N) + elif 0: # Hanning + w = 0.5 + 0.5 * cos(2. * pi * k / N) + else: + w = 1 + z *= w + filtD.append(z) + if center: + # Make a bandpass filter by tuning the low pass filter to new center frequency. + # Make two quadrature filters. + filtI = [] + filtQ = [] + tune = -1j * 2.0 * math.pi * center / rate + NN = len(filtD) + D = (NN - 1.0) / 2.0 + for i in range(NN): + z = 2.0 * cmath.exp(tune * (i - D)) * filtD[i] + filtI.append(z.real) + filtQ.append(z.imag) + return filtI, filtQ + return filtD, filtD + def SetFilterByMode(self, mode): + index = self.modeFilter[mode] + try: + Lab = self.filterButns.buttons[index].GetLabel() + except: + Lab = self.filterButns.buttons[0].GetLabel() + self.filterButns.SetLabel(Lab, True) + def GetFilterCenter(self, mode, bandwidth): + if mode in ('CWU', 'CWL'): + center = max(conf.cwTone, bandwidth // 2) + elif mode in ('LSB', 'USB'): + center = 300 + bandwidth // 2 + elif mode in ('AM',): + center = 0 + elif mode in ('FM',): + center = 0 + elif mode in ('DGT-U', 'DGT-L'): + center = max(1500, bandwidth // 2) + elif mode in ('DGT-IQ', 'DGT-FM'): + center = 0 + elif mode in ('FDV-U', 'FDV-L'): + center = max(1500, bandwidth // 2) + elif mode in ('IMD',): + center = 300 + bandwidth // 2 + else: + center = 300 + bandwidth // 2 + if mode in ('CWL', 'LSB', 'DGT-L', 'FDV-L'): + center = - center + return center + def OnBtnAdjFilter(self, event): + btn = event.GetEventObject() + bw = int(btn.GetLabel()) + self.filterAdjBw1 = bw + if self.filterButns.GetIndex() == 5: + self.OnBtnFilter(event) + def OnBtnFilter(self, event, bw=None): + if event is None: # called by application + self.filterButns.SetLabel(str(bw)) + else: # called by button + btn = event.GetEventObject() + bw = int(btn.GetLabel()) + index = self.filterButns.GetIndex() + mode = self.mode + frate = QS.get_filter_rate(Mode2Index.get(mode, 3), bw) + bw = min(bw, frate // 2) + self.filter_bandwidth = bw + center = self.GetFilterCenter(mode, bw) + # save and restore filter when changing modes + if mode in ('CWU', 'CWL'): + self.modeFilter['CW'] = index + elif mode in ('LSB', 'USB'): + self.modeFilter['SSB'] = index + elif mode in ('AM',): + self.modeFilter['AM'] = index + elif mode in ('FM',): + self.modeFilter['FM'] = index + elif mode in ('DGT-U', 'DGT-L'): + self.modeFilter['DGT'] = index + elif mode in ('DGT-IQ', 'DGT-FM'): + self.modeFilter['DGT'] = index + elif mode in ('FDV-U', 'FDV-L'): + self.modeFilter['FDV'] = index + elif mode in ('IMD',): + self.modeFilter['IMD'] = index + filtI, filtQ = self.MakeFilterCoef(frate, None, bw, center) + lower_edge = center - bw // 2 + QS.set_filters(filtI, filtQ, bw, lower_edge, 0) + self.multi_rx_screen.graph.filter_mode = mode + self.multi_rx_screen.graph.filter_bandwidth = bw + self.multi_rx_screen.graph.filter_center = center + self.multi_rx_screen.waterfall.pane1.filter_mode = mode + self.multi_rx_screen.waterfall.pane1.filter_bandwidth = bw + self.multi_rx_screen.waterfall.pane1.filter_center = center + self.multi_rx_screen.waterfall.pane2.filter_mode = mode + self.multi_rx_screen.waterfall.pane2.filter_bandwidth = bw + self.multi_rx_screen.waterfall.pane2.filter_center = center + if self.screen is self.filter_screen: + self.screen.NewFilter() + def OnFreedvMenu(self, event): + text = '' + for item in self.freedv_menu.GetMenuItems(): + if item.IsChecked(): + text = item.GetItemLabel() + break + for mode, index in conf.freedv_modes: + if mode == text: + break + else: + print ("Failure in OnFreedvMenu") + return + mode = QS.freedv_set_options(mode=index) + if mode == index: + self.freedv_mode = text + elif not self.remote_control_slave: # change to new mode failed + self.freedv_menu_items[mode].Check(1) + pos = (self.width//2, self.height//2) + if index == 8: + dlg = wx.MessageDialog(self.main_frame, "Quisk does not install %s. Please install a system-wide codec2." % text, "FreeDV Modes", wx.OK, pos) + else: + dlg = wx.MessageDialog(self.main_frame, "No codec2 support for " + text, "FreeDV Modes", wx.OK, pos) + dlg.ShowModal() + else: + self.freedv_menu_items[mode].Check(1) + print ("FreeDV change mode failed.") + def OnBtnHelp(self, event): + if event.GetEventObject().GetValue(): + self.OnBtnScreen(None, 'Help') + else: + self.OnBtnScreen(None, self.screenBtnGroup.GetLabel()) + def OnBtnScreen(self, event, name=None): + if event is not None: + win = event.GetEventObject() + name = win.GetLabel() + self.screen.Hide() + self.station_screen.Hide() + if name == 'Config': + self.config_screen.FinishPages() + self.screen = self.config_screen + elif name[0:5] == 'Graph': + self.screen = self.multi_rx_screen + self.screen.ChangeRxZero(True) + self.screen.SetTxFreq(self.txFreq, self.rxFreq) + self.freqDisplay.Display(self.VFO + self.txFreq) + self.screen.PeakHold(name) + self.station_screen.Show() + elif name[0:5] == 'WFall': + self.screen = self.multi_rx_screen + self.screen.ChangeRxZero(False) + self.screen.SetTxFreq(self.txFreq, self.rxFreq) + self.freqDisplay.Display(self.VFO + self.txFreq) + self.screen.PeakHold(name) + sash = self.screen.GetSashPosition() + self.station_screen.Show() + elif name == 'Scope': + if win.direction: # Another push on the same button + self.scope.running = 1 - self.scope.running # Toggle run state + else: # Initial push of button + self.scope.running = 1 + self.screen = self.scope + elif name == 'RX Filter': + self.screen = self.filter_screen + self.freqDisplay.Display(self.screen.txFreq) + self.screen.NewFilter() + elif name == 'Bscope': + self.screen = self.bandscope_screen + self.screen.SetTxFreq(self.txFreq, self.rxFreq) + elif name == 'Audio FFT': + self.screen = self.audio_fft_screen + self.freqDisplay.Display(self.screen.txFreq) + elif name == 'Help': + self.screen = self.help_screen + self.screen.Show() + self.vertBox.Layout() # This destroys the initialized sash position! + self.sliderYs.SetValue(self.screen.y_scale) + self.sliderYz.SetValue(self.screen.y_zero) + self.sliderZo.SetValue(self.screen.zoom_control) + if name[0:5] == 'WFall': + self.screen.SetSashPosition(sash) + def OnBtnFileRecord(self, event): + record = event.GetEventObject().GetValue() + self.config_screen.config.OnFileRecordButton(record) + def ChangeYscale(self, event=None): + self.screen.ChangeYscale(self.sliderYs.GetValue()) + if self.screen == self.multi_rx_screen: + if self.multi_rx_screen.rx_zero == self.waterfall: + self.wfallScaleZ[self.lastBand] = (self.waterfall.y_scale, self.waterfall.y_zero) + self.wfallGrScaleZ[self.lastBand] = (self.waterfall.pane1.y_scale, self.waterfall.pane1.y_zero) + elif self.multi_rx_screen.rx_zero == self.graph: + self.graphScaleZ[self.lastBand] = (self.graph.y_scale, self.graph.y_zero) + def ChangeYzero(self, event=None): + self.screen.ChangeYzero(self.sliderYz.GetValue()) + if self.screen == self.multi_rx_screen: + if self.multi_rx_screen.rx_zero == self.waterfall: + self.wfallScaleZ[self.lastBand] = (self.waterfall.y_scale, self.waterfall.y_zero) + self.wfallGrScaleZ[self.lastBand] = (self.waterfall.pane1.y_scale, self.waterfall.pane1.y_zero) + elif self.multi_rx_screen.rx_zero == self.graph: + self.graphScaleZ[self.lastBand] = (self.graph.y_scale, self.graph.y_zero) + def OnChangeZoom(self, event=None): + zoom_control = self.sliderZo.GetValue() + if self.screen == self.bandscope_screen: + self.bandscope_screen.ChangeZoom(zoom_control) + self.bandscope_screen.SetTxFreq(self.txFreq, self.rxFreq) + return + # The display runs from f1 to f2. The original sample rate is "rate". + # The new effective sample rate is rate * zoom. + # f1 = deltaf + rate * (1 - zoom) / 2 + if zoom_control < 50: + self.zoom = 1.0 # change back to not-zoomed mode + self.zoom_deltaf = 0 + self.zooming = False + else: + a = 1000.0 * self.sample_rate / (self.sample_rate - 2500.0) + self.zoom = 1.0 - zoom_control / a + if not self.zooming: # set deltaf when zoom mode starts + center = self.multi_rx_screen.graph.filter_center + freq = self.rxFreq + center + self.zoom_deltaf = freq + self.zooming = True + zoom = self.zoom + deltaf = self.zoom_deltaf + self.graph.ChangeZoom(zoom, deltaf, zoom_control) + self.waterfall.ChangeZoom(zoom, deltaf, zoom_control) + self.screen.SetTxFreq(self.txFreq, self.rxFreq) + self.station_screen.Refresh() + def OnLevelVOX(self, event): + self.levelVOX = event.GetEventObject().GetValue() + if self.useVOX: + QS.set_tx_audio(vox_level=self.levelVOX) + def OnTimeVOX(self, event): + self.timeVOX = event.GetEventObject().GetValue() + QS.set_tx_audio(vox_time=self.timeVOX) + def OnButtonVOX(self, event): + self.useVOX = event.GetEventObject().GetValue() + if self.useVOX: + QS.set_tx_audio(vox_level=self.levelVOX) + else: + QS.set_tx_audio(vox_level=20) + if self.pttButton.GetValue(): + self.pttButton.SetValue(0, True) + def OnButtonPTT(self, event): + if self.file_play_source == 12 and self.btnFilePlay.GetValue(): # playing CQ file + self.btnFilePlay.SetValue(False, False) + self.file_play_state = 0 # Not playing a file + QS.set_record_state(3) + btn = event.GetEventObject() + if btn.GetValue(): + QS.set_PTT(1) + else: + QS.set_PTT(0) + Hardware.OnButtonPTT(event) + def SetPTT(self, value): + self.pttButton.SetValue(value, False) + if value: + QS.set_PTT(1) + else: + QS.set_PTT(0) + event = wx.PyEvent() + event.SetEventObject(self.pttButton) + Hardware.OnButtonPTT(event) + def OnTxAudioClip(self, event): + v = event.GetEventObject().GetValue() + if self.mode in ('USB', 'LSB'): + self.txAudioClipUsb = v + elif self.mode == 'AM': + self.txAudioClipAm = v + elif self.mode == 'FM': + self.txAudioClipFm = v + elif self.mode in ('FDV-U', 'FDV-L'): + self.txAudioClipFdv = v + else: + return + QS.set_tx_audio(mic_clip=v) + def OnTxAudioPreemph(self, event): + v = event.GetEventObject().GetValue() + if self.mode in ('USB', 'LSB'): + self.txAudioPreemphUsb = v + elif self.mode == 'AM': + self.txAudioPreemphAm = v + elif self.mode == 'FM': + self.txAudioPreemphFm = v + elif self.mode in ('FDV-U', 'FDV-L'): + self.txAudioPreemphFdv = v + else: + return + QS.set_tx_audio(mic_preemphasis = v * 0.01) + def SetTxAudio(self): + if self.mode[0:3] in ('CWL', 'CWU', 'FDV', 'DGT'): + self.CtrlTxAudioClip.slider.Enable(False) + self.CtrlTxAudioPreemph.slider.Enable(False) + else: + self.CtrlTxAudioClip.slider.Enable(True) + self.CtrlTxAudioPreemph.slider.Enable(True) + if self.mode in ('USB', 'LSB'): + clp = self.txAudioClipUsb + pre = self.txAudioPreemphUsb + elif self.mode == 'AM': + clp = self.txAudioClipAm + pre = self.txAudioPreemphAm + elif self.mode == 'FM': + clp = self.txAudioClipFm + pre = self.txAudioPreemphFm + else: + clp = 0 + pre = 0 + QS.set_tx_audio(mic_clip=clp, mic_preemphasis=pre * 0.01) + self.CtrlTxAudioClip.SetValue(clp) + self.CtrlTxAudioPreemph.SetValue(pre) + def OnBtnMute(self, event): + btn = event.GetEventObject() + if btn.GetValue(): + QS.set_volume(0) + else: + QS.set_volume(self.audio_volume) + def OnMultirxPlayBoth(self, event): + QS.set_multirx_play_method(0) + def OnMultirxPlayLeft(self, event): + QS.set_multirx_play_method(1) + def OnMultirxPlayRight(self, event): + QS.set_multirx_play_method(2) + def OnBtnDecimation(self, event=None, rate=None): + if event: + i = event.GetSelection() + rate = Hardware.VarDecimSet(i) + self.vardecim_set = rate + if rate != self.sample_rate: + self.sample_rate = rate + self.multi_rx_screen.ChangeSampleRate(rate) + QS.change_rate(rate, 1) + #print ('FFT size %d, FFT mult %d, average_count %d, rate %d, Refresh %.2f Hz' % ( + # self.fft_size, self.fft_size / self.data_width, average_count, rate, + # float(rate) / self.fft_size / average_count)) + tune = self.txFreq + vfo = self.VFO + self.txFreq = self.VFO = -1 # demand change + self.ChangeHwFrequency(tune, vfo, 'NewDecim') + def ChangeVolume(self, event=None): + # Caution: event can be None + value = self.sliderVol.GetValue() + self.volumeAudio = value + # Simulate log taper pot + B = 50.0 # This controls the gain at mid-volume + x = (B ** (value/1000.0) - 1.0) / (B - 1.0) # x is 0.0 to 1.0 + #print ("Vol %3d %10.6f" % (value, x)) + self.audio_volume = x # audio_volume is 0 to 1.000 + QS.set_volume(x) + def ChangeSidetone(self, event=None): + # Caution: event can be None + value = self.sliderSto.GetValue() + self.sidetone_volume = value + # Simulate log taper pot + B = 50.0 # This controls the gain at mid-volume + x = (B ** (value/1000.0) - 1.0) / (B - 1.0) # x is 0.0 to 1.0 + self.sidetone_0to1 = x + QS.set_sidetone(value, x, self.ritFreq, conf.keyupDelay) + if hasattr(Hardware, 'ChangeSidetone'): + Hardware.ChangeSidetone(x) + def OnRitScale(self, event=None): # Called when the RIT slider is moved + # Caution: event can be None + value = self.ritScale.GetValue() + self.ritButton.SetLabel("RIT %d" % value) + self.ritButton.Refresh() + if self.ritButton.GetValue(): + value = int(value) + self.ritFreq = value + self.graph.ritFreq = value + self.waterfall.pane1.ritFreq = value + self.waterfall.pane2.ritFreq = value + QS.set_tune(self.rxFreq + self.ritFreq, self.txFreq) + QS.set_sidetone(self.sidetone_volume, self.sidetone_0to1, self.ritFreq, conf.keyupDelay) + def OnBtnSplit(self, event): # Called when the Split check button is pressed + self.split_rxtx = self.splitButton.GetValue() + if self.split_rxtx: + if self.split_offset == 0: + if self.mode in ("CWL", "CWU"): + self.split_offset = 1000 + else: + self.split_offset = 3000 + QS.set_split_rxtx(self.split_rxtx_play) + self.txFreq = self.rxFreq + self.split_offset + self.ChangeHwFrequency(self.txFreq, self.VFO, 'OnSplit', event=event, rx_freq=self.rxFreq) + else: + QS.set_split_rxtx(0) + self.split_offset = self.txFreq - self.rxFreq + self.txFreq = self.rxFreq + self.ChangeHwFrequency(self.txFreq, self.VFO, 'OnSplit', event=event) + self.screen.SetTxFreq(self.txFreq, self.rxFreq) + def OnMenuSplitPlay1(self, event): + self.split_rxtx_play = 1 + if self.split_rxtx: + QS.set_split_rxtx(1) + def OnMenuSplitPlay2(self, event): + self.split_rxtx_play = 2 + if self.split_rxtx: + QS.set_split_rxtx(2) + def OnMenuSplitPlay3(self, event): + self.split_rxtx_play = 3 + if self.split_rxtx: + QS.set_split_rxtx(3) + def OnMenuSplitPlay4(self, event): + self.split_rxtx_play = 4 + if self.split_rxtx: + QS.set_split_rxtx(4) + def OnMenuSplitLock(self, event): + menu = self.split_menu + if menu.IsItemChecked('Lock Tx, Split Rx'): + self.split_locktx = True + self.split_lockrx = False + self.splitButton.SetLabel("SplitRx") + elif menu.IsItemChecked('Lock Rx, Split Tx'): + self.split_locktx = False + self.split_lockrx = True + self.splitButton.SetLabel("SplitTx") + else: + self.split_locktx = False + self.split_lockrx = False + self.splitButton.SetLabel("Split") + self.splitButton.Refresh() + def OnMenuSplitRev(self, event): # Called when the Split Reverse button is pressed + if self.split_rxtx: + self.ChangeHwFrequency(self.rxFreq, self.VFO, 'FreqEntry', rx_freq=self.txFreq) + def OnBtnRit(self, event=None): # Called when the RIT check button is pressed + # Caution: event can be None + if self.ritButton.GetValue(): + self.ritFreq = self.ritScale.GetValue() + else: + self.ritFreq = 0 + self.graph.ritFreq = self.ritFreq + self.waterfall.pane1.ritFreq = self.ritFreq + self.waterfall.pane2.ritFreq = self.ritFreq + QS.set_tune(self.rxFreq + self.ritFreq, self.txFreq) + QS.set_sidetone(self.sidetone_volume, self.sidetone_0to1, self.ritFreq, conf.keyupDelay) + def SetRit(self, freq): + if freq: + self.ritButton.SetValue(1) + else: + self.ritButton.SetValue(0) + self.ritScale.SetValue(freq) + self.ritButton.SetLabel("RIT %d" % freq) + self.ritButton.Refresh() + self.OnBtnRit() + def OnBtnFDX(self, event): + btn = event.GetEventObject() + if btn.GetValue(): + QS.set_fdx(1) + if hasattr(Hardware, 'OnBtnFDX'): + Hardware.OnBtnFDX(1) + else: + QS.set_fdx(0) + if hasattr(Hardware, 'OnBtnFDX'): + Hardware.OnBtnFDX(0) + def OnImdSlider(self, event): + value = event.GetEventObject().slider_value + QS.set_imd_level(value) + def OnBtnSpot(self, event): + btn = event.GetEventObject() + self.levelSpot = btn.slider_value + if btn.GetValue(): + value = btn.slider_value + else: + value = -1 + QS.set_spot_level(value) + Hardware.OnSpot(value) + if conf.spot_button_keys_tx: + Hardware.OnButtonPTT(event) + if btn.GetValue(): + QS.set_key_down(1) + else: + QS.set_key_down(0) + def OnBtnTmpRecord(self, event): + btn = event.GetEventObject() + if btn.GetValue(): + self.btnTmpPlay.Enable(0) + QS.set_record_state(0) + else: + self.btnTmpPlay.Enable(1) + QS.set_record_state(1) + def OnBtnTmpPlay(self, event): + btn = event.GetEventObject() + if btn.direction == 1: # left click + if btn.GetValue(): + if QS.is_key_down() and conf.mic_sample_rate != conf.playback_rate: + self.btnTmpPlay.SetValue(False, False) + else: + self.btnTmpRecord.Enable(0) + QS.set_record_state(2) + self.tmp_playing = True + else: + self.btnTmpRecord.Enable(1) + QS.set_record_state(3) + self.tmp_playing = False + else: # right click + btn.SetValue(False) + dr, fn = os.path.split(self.file_name_rec_tmp) + dlg = wx.FileDialog(self.main_frame, 'Choose WAV file', dr, fn, style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT, wildcard="Wave files (*.wav)|*.wav") + if dlg.ShowModal() == wx.ID_OK: + path = dlg.GetPath() + if path[-4:].lower() != '.wav': + path = path + '.wav' + self.file_name_rec_tmp = path + QS.tmp_record_save(path) + dlg.Destroy() + def OnBtnFilePlay(self, event): + btn = event.GetEventObject() + enable = btn.GetValue() + self.config_screen.config.OnFilePlayButton(enable) + if enable: + self.file_play_state = 1 # Start playing a file + if self.file_play_source == 10: # Play speaker audio file + QS.set_record_state(5) + elif self.file_play_source == 11: # Play sample file + QS.set_record_state(6) + elif self.file_play_source == 12: # Play CQ file + QS.set_record_state(5) + self.SetPTT(True) + else: + self.file_play_state = 0 # Not playing a file + QS.set_record_state(3) + if self.file_play_source == 12: # Play CQ file + self.SetPTT(False) + def TurnOffFilePlay(self): + self.btnFilePlay.SetValue(False, False) + self.file_play_state = 0 # Not playing a file + QS.set_record_state(3) + def OnBtnTest1(self, event): + btn = event.GetEventObject() + if btn.GetValue(): + QS.add_tone(10000) + else: + QS.add_tone(0) + def OnBtnTest2(self, event): + return + def OnBtnColorDialog(self, event): + btn = event.GetEventObject() + dlg = wx.ColourDialog(self.main_frame) + dlg.GetColourData().SetChooseFull(True) + if dlg.ShowModal() == wx.ID_OK: + data = dlg.GetColourData() + print (data.GetColour().Get(False)) + btn.text_color = data.GetColour().Get(False) + btn.Refresh() + dlg.Destroy() + def OnBtnColor(self, event): + if not self.color_list: + clist = wx.lib.colourdb.getColourInfoList() + self.color_list = [(0, clist[0][0])] + self.color_index = 0 + for i in range(1, len(clist)): + if self.color_list[-1][1].replace(' ', '') != clist[i][0].replace(' ', ''): + #if 'BLUE' in clist[i][0]: + self.color_list.append((i, clist[i][0])) + btn = event.GetEventObject() + if btn.shift: + del self.color_list[self.color_index] + else: + self.color_index += btn.direction + if self.color_index >= len(self.color_list): + self.color_index = 0 + elif self.color_index < 0: + self.color_index = len(self.color_list) -1 + color = self.color_list[self.color_index][1] + print(self.color_index, color) + #self.main_frame.SetBackgroundColour(color) + #self.main_frame.Refresh() + #self.screen.Refresh() + #btn.SetBackgroundColour(color) + btn.text_color = color + btn.Refresh() + def OnBtnAGC(self, event): + btn = self.BtnAGC + self.levelOffAGC = btn.slider_value_off + self.levelAGC = btn.slider_value_on + value = btn.GetValue() + if value: + level = self.levelAGC + else: + level = self.levelOffAGC + # Simulate log taper pot. Volume is 0 to 1. + x = (10.0 ** (float(level) * 0.003000434077) - 0.99999) / 1000.0 + QS.set_agc(x * conf.agc_max_gain) + def OnBtnSquelch(self, event=None): + btn = self.BtnSquelch + value = btn.GetValue() + if self.mode == 'FM': + self.levelSquelch = btn.slider_value + if value: + QS.set_squelch(self.levelSquelch / 12.0 - 120.0) + else: + QS.set_squelch(-999.0) + elif self.mode[0:3] == 'FDV': + if value: + QS.freedv_set_squelch_en(1) + else: + QS.freedv_set_squelch_en(0) + else: + self.levelSquelchSSB = btn.slider_value + if value: + QS.set_ssb_squelch(1, self.levelSquelchSSB) + else: + QS.set_ssb_squelch(0, self.levelSquelchSSB) + def OnBtnAutoNotch(self, event): + if event.GetEventObject().GetValue(): + QS.set_auto_notch(1) + else: + QS.set_auto_notch(0) + def OnBtnNR2(self, event): + btn = event.GetEventObject() + if btn.GetValue(): + self.wdsp.put(self.wdsp.SetRXAEMNRRun, self.wdsp_channel, 1) + QS.wdsp_set_parameter(self.wdsp_channel, in_use=1) + else: + self.wdsp.put(self.wdsp.SetRXAEMNRRun, self.wdsp_channel, 0) + def OnMenuNB(self, event): + # menu is ("NB 1", "NB 2", "NB 3", "SNB") + for item in self.NB_menu.GetMenuItems(): + if item.IsChecked(): + text = item.GetItemLabel() + self.btnNB.SetLabel(text) + self.btnNB.Refresh() + break + self.OnBtnNB(None) + def OnBtnNB(self, event): + text = self.btnNB.GetLabel() + if self.btnNB.GetValue(): # Noise blanker On + if text == "SNB": + QS.set_noise_blanker(0) + self.wdsp.put(self.wdsp.SetRXASNBARun, self.wdsp_channel, 1) + QS.wdsp_set_parameter(self.wdsp_channel, in_use=1) + else: + self.wdsp.put(self.wdsp.SetRXASNBARun, self.wdsp_channel, 0) + index = int(text[-1]) + QS.set_noise_blanker(index) + else: # Noise blanker Off + QS.set_noise_blanker(0) + self.wdsp.put(self.wdsp.SetRXASNBARun, self.wdsp_channel, 0) + def FreqEntry(self, event): + freq = event.GetString() + win = event.GetEventObject() + win.Clear() + if not freq: + return + try: + freq = str2freq (freq) + except ValueError: + pass + else: + tune = freq % 10000 + vfo = freq - tune + self.BandFromFreq(freq) + self.ChangeHwFrequency(tune, vfo, 'FreqEntry') + def ChangeHwFrequency(self, tune, vfo, source='', band='', event=None, rx_freq=None): + """Change the VFO and tuning frequencies, and notify the hardware. + + tune: the new tuning frequency in +- sample_rate/2; + vfo: the new vfo frequency in Hertz; this is the RF frequency at zero Hz audio + source: a string indicating the source or widget requesting the change; + band: if source is "BtnBand", the band requested; + event: for a widget, the event (used to access control/shift key state). + rx_freq: the new changed value of App.rxFreq, the receive frequency in +- sample_rate/2 + + Try to update the hardware by calling Hardware.ChangeFrequency(). + The hardware will reply with the updated frequencies which may be different + from those requested; use and display the returned tune and vfo. + """ + if not self.split_rxtx: + self.rxFreq = tune # rxFreq must be correct before the call to Hardware.ChangeFrequency() + elif rx_freq is not None: + self.rxFreq = rx_freq + if self.screen == self.bandscope_screen: + freq = vfo + tune + tune = freq % 10000 + vfo = freq - tune + tune, vfo = Hardware.ChangeFrequency(vfo + tune, vfo, source, band, event) + self.ChangeDisplayFrequency(tune - vfo, vfo, rx_freq is not None) + def ChangeDisplayFrequency(self, tune, vfo, new_rxfreq=False): + 'Change the frequency displayed by Quisk' + change = 0 + if tune != self.txFreq or new_rxfreq: + change = 1 + self.txFreq = tune + if not self.split_rxtx: + self.rxFreq = self.txFreq + if self.screen == self.bandscope_screen: + self.screen.SetFrequency(tune + vfo) + else: + self.screen.SetTxFreq(self.txFreq, self.rxFreq) + QS.set_tune(self.rxFreq + self.ritFreq, self.txFreq) + if vfo != self.VFO: + change = 1 + self.VFO = vfo + self.graph.SetVFO(vfo) + self.waterfall.SetVFO(vfo) + self.station_screen.Refresh() + if self.w_phase: + self.w_phase.Redraw() + if change: + if conf.name_of_sound_capt or conf.name_of_mic_play: + ampl, phase = self.GetAmplPhase('rx') + QS.set_ampl_phase(ampl, phase, 0) + ampl, phase = self.GetAmplPhase('tx') + QS.set_ampl_phase(ampl, phase, 1) + self.freqDisplay.Display(self.txFreq + self.VFO) + self.fldigi_new_freq = self.txFreq + self.VFO + return change + def ChangeRxTxFrequency(self, rx_freq=None, tx_freq=None): + if not self.split_rxtx and not tx_freq: + tx_freq = rx_freq + if tx_freq: + tune = tx_freq - self.VFO + d = self.sample_rate * 45 // 100 + if -d <= tune <= d: # Frequency is on-screen + vfo = self.VFO + else: # Change the VFO + vfo = (tx_freq // 5000) * 5000 - 5000 + tune = tx_freq - vfo + self.BandFromFreq(tx_freq) + self.ChangeHwFrequency(tune, vfo, 'FreqEntry') + if rx_freq and self.split_rxtx: # Frequency must be on-screen + tune = rx_freq - self.VFO + self.ChangeHwFrequency(self.txFreq, self.VFO, 'FreqEntry', rx_freq=tune) + def OnBtnMode(self, event): + mode = self.modeButns.GetLabel() + delta = 0 # Change frequency so switch between CW and SSB keeps tone constant + if self.mode == 'USB': + if mode in ('CWL', 'CWU'): + delta = 1 + elif self.mode == 'LSB': + if mode in ('CWL', 'CWU'): + delta = -1 + elif self.mode in ('CWL', 'CWU'): + if mode == 'USB': + delta = -1 + elif mode == 'LSB': + delta = 1 + if delta: + delta *= conf.cwTone + self.ChangeRxTxFrequency(self.rxFreq + delta + self.VFO, self.txFreq + delta + self.VFO) + Hardware.ChangeMode(mode) + self.mode = mode + self.MakeFilterButtons(self.Mode2Filters(mode)) + QS.set_rx_mode(Mode2Index.get(mode, 3)) + if mode == 'CWL': + self.SetRit(conf.cwTone) + elif mode == 'CWU': + self.SetRit(-conf.cwTone) + else: + self.SetRit(0) + if mode in ('CWL', 'CWU'): + self.SetFilterByMode('CW') + elif mode in ('LSB', 'USB'): + self.SetFilterByMode('SSB') + elif mode == 'AM': + self.SetFilterByMode('AM') + elif mode == 'FM': + self.SetFilterByMode('FM') + elif mode[0:4] == 'DGT-': + self.SetFilterByMode('DGT') + elif mode[0:4] == 'FDV-': + self.SetFilterByMode('FDV') + elif mode == 'IMD': + self.SetFilterByMode('IMD') + elif mode == conf.add_extern_demod: + self.SetFilterByMode(conf.add_extern_demod) + self.sliderSquelch.DeleteSliderWindow() + if mode == 'FM': + self.sliderSquelch.SetSlider(self.levelSquelch) + else: + self.sliderSquelch.SetSlider(self.levelSquelchSSB) + self.OnBtnSquelch() + if mode not in ('FDV-L', 'FDV-U'): + self.graph.SetDisplayMsg() + self.waterfall.SetDisplayMsg() + self.SetTxAudio() + def MakeMemPopMenu(self): + self.memory_menu.Destroy() + self.memory_menu = wx.Menu() + for data in self.memoryState: + txt = FreqFormatter(data[0]) + item = self.memory_menu.Append(-1, txt) + self.Bind(wx.EVT_MENU, self.OnPopupMemNext, item) + def OnPopupMemNext(self, event): + frq = self.memory_menu.GetLabel(event.GetId()) + frq = frq.replace(' ','') + frq = int(frq) + for freq, band, vfo, txfreq, mode in self.memoryState: + if freq == frq: + break + else: + return + if band == self.lastBand: # leave band unchanged + self.modeButns.SetLabel(mode, True) + self.ChangeHwFrequency(txfreq, vfo, 'FreqEntry') + else: # change to new band + self.bandState[band] = (vfo, txfreq, mode) + self.bandBtnGroup.SetLabel(band, do_cmd=True) + def OnBtnMemSave(self, event): + frq = self.VFO + self.txFreq + for i in range(len(self.memoryState)): + data = self.memoryState[i] + if data[0] == frq: + self.memoryState[i] = (self.VFO + self.txFreq, self.lastBand, self.VFO, self.txFreq, self.mode) + return + self.memoryState.append((self.VFO + self.txFreq, self.lastBand, self.VFO, self.txFreq, self.mode)) + self.memoryState.sort() + self.memNextButton.Enable(True) + self.memDeleteButton.Enable(True) + self.MakeMemPopMenu() + self.station_screen.Refresh() + def OnBtnMemNext(self, event): + frq = self.VFO + self.txFreq + for freq, band, vfo, txfreq, mode in self.memoryState: + if freq > frq: + break + else: + freq, band, vfo, txfreq, mode = self.memoryState[0] + if band == self.lastBand: # leave band unchanged + self.modeButns.SetLabel(mode, True) + self.ChangeHwFrequency(txfreq, vfo, 'FreqEntry') + else: # change to new band + self.bandState[band] = (vfo, txfreq, mode) + self.bandBtnGroup.SetLabel(band, do_cmd=True) + def OnBtnMemDelete(self, event): + frq = self.VFO + self.txFreq + for i in range(len(self.memoryState)): + data = self.memoryState[i] + if data[0] == frq: + del self.memoryState[i] + break + self.memNextButton.Enable(bool(self.memoryState)) + self.memDeleteButton.Enable(bool(self.memoryState)) + self.MakeMemPopMenu() + self.station_screen.Refresh() + def OnRightClickMemory(self, event): + event.Skip() + pos = event.GetPosition() + self.memNextButton.PopupMenu(self.memory_menu, pos) + def OnBtnFavoritesShow(self, event): + self.screenBtnGroup.SetLabel("Config", do_cmd=False) + self.screen.Hide() + self.config_screen.FinishPages() + self.screen = self.config_screen + self.config_screen.notebook.SetSelection(2) + self.screen.Show() + self.vertBox.Layout() # This destroys the initialized sash position! + def OnBtnFavoritesNew(self, event): + self.config_screen.favorites.AddNewFavorite() + self.OnBtnFavoritesShow(event) + def OnBtnBand(self, event): + band = self.lastBand # former band in use + try: + f1, f2 = conf.BandEdge[band] + if f1 <= self.VFO + self.txFreq <= f2: + self.bandState[band] = (self.VFO, self.txFreq, self.mode) + except KeyError: + pass + btn = event.GetEventObject() + band = btn.GetLabel() # new band + self.lastBand = band + try: + vfo, tune, mode = self.bandState[band] + except KeyError: + vfo, tune, mode = (1000000, 0, 'LSB') + if band == '60': + if self.mode in ('CWL', 'CWU'): + freq60 = [] + for f in conf.freq60: + freq60.append(f + 1500) + else: + freq60 = conf.freq60 + freq = vfo + tune + if btn.direction: + vfo = self.VFO + if 5100000 < vfo < 5600000: + if btn.direction > 0: # Move up + for f in freq60: + if f > vfo + self.txFreq: + freq = f + break + else: + freq = freq60[0] + else: # move down + l = list(freq60) + l.reverse() + for f in l: + if f < vfo + self.txFreq: + freq = f + break + else: + freq = freq60[-1] + half = self.sample_rate // 2 * self.graph_width // self.data_width + while freq - vfo <= -half + 1000: + vfo -= 10000 + while freq - vfo >= +half - 5000: + vfo += 10000 + tune = freq - vfo + elif band == 'Time': + vfo, tune, mode = conf.bandTime[btn.index] + self.modeButns.SetLabel(mode, True) + self.txFreq = self.VFO = -1 # demand change + self.ChangeBand(band) + self.ChangeHwFrequency(tune, vfo, 'BtnBand', band=band) + if band in ('Time', 'Audio') or conf.tx_level.get(band, 127) == 0: + self.pttButton.Enable(False) + else: + self.pttButton.Enable(True) + def BandFromFreq(self, frequency): # Change to a new band based on the frequency + if self.screen == self.bandscope_screen: + return + try: + f1, f2 = conf.BandEdge[self.lastBand] + if f1 <= frequency <= f2: + return # We are within the current band + except KeyError: + f1 = f2 = -1 + # Frequency is not within the current band. Save the current band data. + if f1 <= self.VFO + self.txFreq <= f2: + self.bandState[self.lastBand] = (self.VFO, self.txFreq, self.mode) + # Change to the correct band based on frequency. + for band in conf.BandEdge: + f1, f2 = conf.BandEdge[band] + if f1 <= frequency <= f2: + self.lastBand = band + self.bandBtnGroup.SetLabel(band, do_cmd=False) + try: + vfo, tune, mode = self.bandState[band] + except KeyError: + vfo, tune, mode = (0, 0, 'LSB') + self.modeButns.SetLabel(mode, True) + self.ChangeBand(band) + break + def ChangeBand(self, band): + if self.remote_control_head: + self.Hardware.RemoteCtlSend(f'JsonAppFunc;{json.dumps(("ChangeBand", band))}\n') + Hardware.ChangeBand(band) + s, z = self.graphScaleZ.get(band, (conf.graph_y_scale, conf.graph_y_zero)) + self.waterfall.SetPane1(self.wfallGrScaleZ.get(band, (s, z))) + self.waterfall.SetPane2(self.wfallScaleZ.get(band, (conf.waterfall_y_scale, conf.waterfall_y_zero))) + self.graph.ChangeYscale(s) + self.graph.ChangeYzero(z) + if self.screen == self.multi_rx_screen and self.multi_rx_screen.rx_zero in (self.waterfall, self.graph): + self.sliderYs.SetValue(self.screen.y_scale) + self.sliderYz.SetValue(self.screen.y_zero) + def OnBtnUpDnBandDelta(self, event, is_band_down): + sample_rate = int(self.sample_rate * self.zoom) + oldvfo = self.VFO + btn = event.GetEventObject() + if btn.direction > 0: # left button was used, move a bit + d = int(sample_rate // 9) + else: # right button was used, move to edge + d = int(sample_rate * 45 // 100) + if is_band_down: + d = -d + vfo = self.VFO + d + if sample_rate > 40000: + vfo = (vfo + 5000) // 10000 * 10000 # round to even number + delta = 10000 + elif sample_rate > 5000: + vfo = (vfo + 500) // 1000 * 1000 + delta = 1000 + else: + vfo = (vfo + 50) // 100 * 100 + delta = 100 + if oldvfo == vfo: + if is_band_down: + d = -delta + else: + d = delta + else: + d = vfo - oldvfo + self.VFO += d + self.txFreq -= d + self.rxFreq -= d + # Set the display but do not change the hardware + self.graph.SetVFO(self.VFO) + self.waterfall.SetVFO(self.VFO) + self.station_screen.Refresh() + self.screen.SetTxFreq(self.txFreq, self.rxFreq) + self.freqDisplay.Display(self.txFreq + self.VFO) + def OnBtnDownBand(self, event): + self.band_up_down = 1 + self.OnBtnUpDnBandDelta(event, True) + def OnBtnUpBand(self, event): + self.band_up_down = 1 + self.OnBtnUpDnBandDelta(event, False) + def OnBtnUpDnBandDone(self, event): + self.band_up_down = 0 + tune = self.txFreq + vfo = self.VFO + self.txFreq = self.VFO = -1 # Force an update + self.ChangeHwFrequency(tune, vfo, 'BtnUpDown') + def SearchFreqAmPh(self, freq, data, index): + # If index is -1, search data for the VFO frequency. + # Otherwise, search in the frequencies for the VFO at index. + # The list must be sorted. Search for a bracket lst[i] <= freq <= lst[i + 1] + # Return 0, 1 for freq <= lst[0]; return len(lst) - 2, len(lst) - 1 for freq >= lst[-1] + # Also return the length. + if index < 0: # VFO frequency is at data[i][0] + items = data + else: # The Rx/Tx frequency for the VFO at index is data[index][1][i][0] + items = data[index][1] + length = len(items) + if length == 0: + return 0, 0, 0 + if length == 1: + return 1, 0, 0 + f1 = items[0][0] + if freq <= f1: # before first frequency + return length, 0, 1 + for i in range(1, length): + f1 = items[i - 1][0] + f2 = items[i][0] + if f1 <= freq <= f2: + return length, i - 1, i + return length, length - 2, length - 1 # after last frequency + def GetAmplPhase(self, rx_tx): + band = self.lastBand + data = self.bandAmplPhase.get(band, {}) + data = data.get(rx_tx, []) + if not data: # No corrections for this band + return 0.0, 0.0 + if rx_tx == 'rx': + Freq0 = self.rxFreq + else: + Freq0 = self.txFreq + nVFO, iVfo1, iVfo2 = self.SearchFreqAmPh(self.VFO, data, -1) + #print ('iVfo1', iVfo1, 'iVfo2', iVfo2) + nFreqV1, iV1F1, iV1F2 = self.SearchFreqAmPh(Freq0, data, iVfo1) + #print (' V1', iV1F1, iV1F2, data[iVfo1][1][iV1F1]) + if iVfo1 == iVfo2: + nFreqV2 = nFreqV1 + iV2F1 = iV1F1 + iV2F2 = iV1F2 + else: + nFreqV2, iV2F1, iV2F2 = self.SearchFreqAmPh(Freq0, data, iVfo2) + #print (' V2', iV2F1, iV2F2, data[iVfo2][1][iV2F1]) + #print ("nVFO", nVFO, "nFreq", nFreqV1, nFreqV2) + try: # guard against divide by zero + if nVFO == 1: + if nFreqV1 == 1: # duplicate old logic + freq, ampl, phase = data[iVfo1][1][0] + #print ("new %10.6f %10.6f" % (ampl, phase)) + else: # linear interpolation/extrapolation of Freq0 + freq1, ampl1, phase1 = data[iVfo1][1][iV1F1] + freq2, ampl2, phase2 = data[iVfo1][1][iV1F2] + t = (Freq0 - freq1) / (freq2 - freq1) # linear interpolation + ampl = (1.0 - t) * ampl1 + t * ampl2 + phase = (1.0 - t) * phase1 + t * phase2 + #print ("new %10.6f %10.6f" % (ampl, phase)) + elif nFreqV1 == 1 and nFreqV2 == 1: # duplicate old logic + fVfo1 = data[iVfo1][0] # linear interpolation/extrapolation of self.VFO + fVfo2 = data[iVfo2][0] + freq1, ampl1, phase1 = data[iVfo1][1][0] + freq2, ampl2, phase2 = data[iVfo2][1][0] + t = (self.VFO - fVfo1) / (fVfo2 - fVfo1) + ampl = (1.0 - t) * ampl1 + t * ampl2 + phase = (1.0 - t) * phase1 + t * phase2 + #print ("new %10.6f %10.6f" % (ampl, phase)) + else: + fVfo1 = data[iVfo1][0] + fVfo2 = data[iVfo2][0] + freq11, ampl11, phase11 = data[iVfo1][1][iV1F1] + freq12, ampl12, phase12 = data[iVfo1][1][iV1F2] + freq21, ampl21, phase21 = data[iVfo2][1][iV2F1] + freq22, ampl22, phase22 = data[iVfo2][1][iV2F2] + #print ("Freq0", Freq0) + #print ("Try Vfo 1 %d freq %d %d" % (fVfo1, freq11, freq12)) + #print ("Try Vfo 2 %d freq %d %d" % (fVfo2, freq21, freq22)) + if freq11 == freq21 and freq12 == freq22: # rectangular box: bilinear interpolation + #print ("Vfo %d %d %d freq %d %d %d" % (fVfo1, self.VFO, fVfo2, freq11, Freq0, freq12)) + t = (Freq0 - freq11) / (freq12 - freq11) + u = (self.VFO - fVfo1) / (fVfo2 - fVfo1) + #print ("%10.4f %10.4f" % (t, u)) + #print (ampl11, ampl12, ampl22, ampl21) + #print (phase11, phase12, phase22, phase21) + ampl = (1.0 - t) * (1.0 - u) * ampl11 + t * (1.0 - u) * ampl12 + t * u * ampl22 + (1.0 - t) * u * ampl21 + phase = (1.0 - t) * (1.0 - u) * phase11 + t * (1.0 - u) * phase12 + t * u * phase22 + (1.0 - t) * u * phase21 + #print ('Box', fVfo1, fVfo2, freq11, freq12) + #print ("new B %10.6f %10.6f" % (ampl, phase)) + else: # linear interpolation/extrapolation of Freq0 at nearest VFO + if abs(self.VFO - fVfo1) <= abs(self.VFO - fVfo2): + nFreq = nFreqV1 # Use VFO 1 + freq1, ampl1, phase1 = data[iVfo1][1][iV1F1] + #print ('Vfo1', fVfo1, freq1) + if nFreq > 1: + freq2, ampl2, phase2 = data[iVfo1][1][iV1F2] + else: # Use VFO 2 + nFreq = nFreqV2 + freq1, ampl1, phase1 = data[iVfo2][1][iV2F1] + #print ('Vfo2', fVfo2, freq1) + if nFreq > 1: + freq2, ampl2, phase2 = data[iVfo2][1][iV2F2] + if nFreq == 1: + ampl, phase = ampl1, phase1 + #print ("new %10.6f %10.6f" % (ampl, phase)) + else: # linear interpolation/extrapolation of Freq0 + #print ('Lin', freq1, freq2) + t = (Freq0 - freq1) / (freq2 - freq1) # linear interpolation + ampl = (1.0 - t) * ampl1 + t * ampl2 + phase = (1.0 - t) * phase1 + t * phase2 + #print ("new %10.6f %10.6f" % (ampl, phase)) + except: + traceback.print_exc() + return 0.0, 0.0 + #print ("new %10.6f %10.6f" % (ampl, phase)) + return ampl, phase + def PostStartup(self): # called once after sound attempts to start + self.config_screen.OnGraphData(None) # update config in case sound is not running + #txt = self.sound_thread.config_text # change config_text if StartSamples() returns a string + #if txt: + # self.config_text = txt + # self.main_frame.SetConfigText(txt) + def FldigiPoll(self): # Keep Quisk and Fldigi frequencies equal; control Fldigi PTT from Quisk + if self.fldigi_server is None: + return + if self.fldigi_new_freq: # Our frequency changed; send to fldigi + try: + self.fldigi_server.main.set_frequency(float(self.fldigi_new_freq)) + except: + # traceback.print_exc() + pass + self.fldigi_new_freq = None + self.fldigi_timer = time.time() + return + try: + freq = self.fldigi_server.main.get_frequency() + except: + # traceback.print_exc() + return + else: + freq = int(freq + 0.5) + try: + rxtx = self.fldigi_server.main.get_trx_status() # returns rx, tx, tune + except: + return + if time.time() - self.fldigi_timer < 0.3: # If timer is small, change originated in Quisk + self.fldigi_rxtx = rxtx + self.fldigi_freq = freq + return + if self.fldigi_freq != freq: + self.fldigi_freq = freq + #print "Change freq", freq + self.ChangeRxTxFrequency(None, freq) + self.fldigi_new_freq = None + if self.fldigi_rxtx != rxtx: + self.fldigi_rxtx = rxtx + #print 'Fldigi changed to', rxtx + if rxtx == 'rx': + self.pttButton.SetValue(0, True) + else: + self.pttButton.SetValue(1, True) + self.fldigi_timer = time.time() + else: + if QS.is_key_down(): + if rxtx == 'rx': + self.fldigi_server.main.tx() + self.fldigi_timer = time.time() + else: # key is up + if rxtx != 'rx': + self.fldigi_server.main.rx() + self.fldigi_timer = time.time() + def HamlibPoll(self): # Poll for Hamlib commands + if self.hamlib_socket: + try: # Poll for new client connections. + conn, address = self.hamlib_socket.accept() + except socket.error: + pass + else: + # print ('Connection from', address) + self.hamlib_clients.append(HamlibHandlerRig2(self, conn, address)) + for client in self.hamlib_clients: # Service existing clients + if not client.Process(): # False return indicates a closed connection; remove the handler for this client + self.hamlib_clients.remove(client) + # print 'Remove', client.address + break + def OnKeyHook(self, event): + event.Skip() + if conf.hot_key_ptt_if_hidden: # Hot key PTT operates even if Quisk is hidden + return # Poll key with HotKeyPoll() + ptt1 = conf.hot_key_ptt1 + if not ptt1: + return + if 97 <= ptt1 <= 122: # Lower case + ptt1 -= 32 # Convert to upper case + if event.GetKeyCode() == ptt1: + ptt2 = conf.hot_key_ptt2 + hit = False + if ptt2 is None or ptt2 == wx.ACCEL_NORMAL: + hit = True + elif ptt2 == wx.ACCEL_SHIFT: + hit = event.ShiftDown() + elif ptt2 == wx.ACCEL_CTRL: + hit = event.ControlDown() + elif ptt2 == wx.ACCEL_ALT: + hit = event.AltDown() + elif ptt2 == wx.ACCEL_SHIFT | wx.ACCEL_CTRL: + hit = event.ShiftDown() and event.ControlDown() + else: + hit = True + if hit: # Repeated key press events will occur + self.hot_key_ptt_pressed = True + def HotKeyPoll(self): # This is only used if the hot key DOES work when Quisk is hidden. + if self.QuiskGetKeyState(conf.hot_key_ptt1): + ptt2 = conf.hot_key_ptt2 + if ptt2 is None or ptt2 == wx.ACCEL_NORMAL: + self.hot_key_ptt_is_down = True + elif ptt2 == wx.ACCEL_SHIFT: + self.hot_key_ptt_is_down = wx.GetKeyState(wx.WXK_SHIFT) + elif ptt2 == wx.ACCEL_CTRL: + self.hot_key_ptt_is_down = wx.GetKeyState(wx.WXK_CONTROL) + elif ptt2 == wx.ACCEL_ALT: + self.hot_key_ptt_is_down = wx.GetKeyState(wx.WXK_ALT) + elif ptt2 == wx.ACCEL_SHIFT | wx.ACCEL_CTRL: + self.hot_key_ptt_is_down = wx.GetKeyState(wx.WXK_SHIFT) and wx.GetKeyState(wx.WXK_CONTROL) + else: + self.hot_key_ptt_is_down = True + else: + self.hot_key_ptt_is_down = False + if self.hot_key_ptt_is_down and not self.hot_key_ptt_was_down: + self.hot_key_ptt_pressed = True + def OnReadSound(self): # called at frequent intervals + #if sys.platform == 'win32': + # self.main_frame.Update() + if self.hamlib_com1_handler: + self.hamlib_com1_handler.Process() + if self.hamlib_com2_handler: + self.hamlib_com2_handler.Process() + if self.use_fast_heart_beat: + Hardware.FastHeartBeat() + if conf.do_repeater_offset: + hold = QS.tx_hold_state(-1) + if hold == 2: # Tx is being held for an FM repeater TX frequency shift + rdict = self.config_screen.favorites.RepeaterDict + freq = self.txFreq + self.VFO + freq = ((freq + 500) // 1000) * 1000 + if freq in rdict: + offset, tone = rdict[freq] + QS.set_ctcss(tone) + Hardware.RepeaterOffset(offset) + for i in range(100): + time.sleep(0.010) + if Hardware.RepeaterOffset(): + break + QS.tx_hold_state(3) + elif hold == 4: # No delay necessary on key up + Hardware.RepeaterOffset(0) + QS.set_ctcss(0) + QS.tx_hold_state(1) + if QS.is_key_down(): # Tx indicator + if not self.tx_indicator: + self.tx_indicator = True + self.pttButton.Tx.TurnOn(True) + else: + if self.tx_indicator: + self.tx_indicator = False + self.pttButton.Tx.TurnOn(False) + if True: # Manage the PTT button using serial port, VOX, hot keys, WAV file play, PTT button, MIDI, CAT + ptt_button_down = self.pttButton.GetValue() + ptt = None + if conf.quisk_serial_cts[0:4] == "PTT " or conf.quisk_serial_dsr[0:4] == "PTT ": + old = self.serial_ptt_active + if self.file_play_state == 0: + if QS.get_params("serial_ptt"): + ptt = True + self.serial_ptt_active = True + elif self.serial_ptt_active: + ptt = False + self.serial_ptt_active = False + elif self.file_play_state == 2 and QS.get_params("serial_ptt"): + self.TurnOffFilePlay() + ptt = True + self.serial_ptt_active = True + if self.remote_control_head and old != self.serial_ptt_active: + Hardware.RemoteCtlSend('PTT;%d\n' % self.serial_ptt_active) + if self.useVOX: + if self.file_play_state == 0: + if QS.is_vox(): + ptt = True + self.vox_ptt_active = True + elif self.vox_ptt_active: + ptt = False + self.vox_ptt_active = False + elif self.file_play_state == 2 and QS.is_vox(): # VOX tripped between file play repeats + self.TurnOffFilePlay() + ptt = True + self.vox_ptt_active = True + if self.file_play_state == 2 and QS.is_key_down(): # hardware key between file play repeats + if time.time() > self.file_play_timer - self.file_play_repeat + 0.25: # pause to allow key state to change + self.TurnOffFilePlay() + ptt = False + if conf.hot_key_ptt1 and self.screen != self.config_screen: + if conf.hot_key_ptt_if_hidden: # hot key PTT operates even if Quisk is hidden + self.HotKeyPoll() + if conf.hot_key_ptt_toggle: + if self.hot_key_ptt_pressed: # Key down event was received + ptt = not ptt_button_down + if ptt: + self.TurnOffFilePlay() + else: + if self.hot_key_ptt_pressed: # Multiple key down events are being received + ptt = True + self.hot_key_ptt_active = True + if not ptt_button_down: + self.TurnOffFilePlay() + elif self.hot_key_ptt_active and not self.QuiskGetKeyState(conf.hot_key_ptt1): + ptt = False + self.hot_key_ptt_active = False + self.hot_key_ptt_was_down = self.hot_key_ptt_is_down + self.hot_key_ptt_pressed = False + if ptt is True and not ptt_button_down: + self.SetPTT(True) + elif ptt is False and ptt_button_down: + self.SetPTT(False) + if self.want_RxTx: + x = QS.is_key_down() # Must be 0 or 1 + if self.old_RxTx != x: + self.old_RxTx = x + Hardware.OnChangeRxTx(x) + self.timer = time.time() + if self.bandscope_clock: # Hermes UDP protocol + data = QS.get_bandscope(self.bandscope_clock, self.bandscope_screen.zoom, float(self.bandscope_screen.zoom_deltaf)) + if data and self.screen == self.bandscope_screen: + self.screen.OnGraphData(data) + if self.screen == self.scope: + # Get raw data, not FFT + data = QS.get_graph(0, 1.0, 0) + if data: + self.scope.OnGraphData(data) # Send message to draw new data + return 1 # we got new graph/scope data + elif self.screen == self.audio_fft_screen: + QS.get_graph(2, self.zoom, float(self.zoom_deltaf)) # discard data + audio_data = QS.get_audio_graph() # Display the audio FFT + if audio_data: + self.screen.OnGraphData(audio_data) + else: + data = QS.get_graph(1, self.zoom, float(self.zoom_deltaf)) # get FFT data + if data: + #T('') + if self.remote_control_slave: + Hardware.RemoteCtlSend("M;%s\n" % self.smeter.GetLabel()) + if self.remote_control_head: + self.smeter.SetLabel(Hardware.GetSmeter()) + elif self.screen == self.bandscope_screen: + d = QS.get_hermes_adc() # ADC level from bandscope, 0.0 to 1.0 + if d < 1E-10: + d = 1E-10 + self.smeter.SetLabel(" ADC %.0f%% %.0fdB" % (d * 100.0, 20 * math.log10(d))) + elif self.smeter_usage == "smeter": # update the S-meter + if self.mode in ('FDV-U', 'FDV-L'): + self.NewDVmeter() + else: + self.NewSmeter() + elif self.smeter_usage == "freq": + self.MeasureFrequency() # display measured frequency + else: + self.MeasureAudioVoltage() # display audio voltage + if self.screen == self.config_screen: + pass + elif self.screen == self.bandscope_screen: + pass + else: + self.screen.OnGraphData(data) # Send message to draw new data + #T('graph data') + #application.Yield() + #T('Yield') + return 1 # We got new graph/scope data + data, index = QS.get_multirx_graph() # get FFT data for sub-receivers + if data: + self.multi_rx_screen.OnGraphData(data, index) + if QS.get_overrange(): + self.clip_time0 = self.timer + self.freqDisplay.Clip(1) + if self.clip_time0: + if self.timer - self.clip_time0 > 1.0: + self.clip_time0 = 0 + self.freqDisplay.Clip(0) + if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks: + self.heart_time0 = self.timer + Hardware.HeartBeat() + msg = QS.GetQuiskPrintf() + if msg: + print(msg, end='') + if self.screen == self.config_screen: + self.screen.OnGraphData() # Send message to draw new data + if self.add_version and Hardware.GetFirmwareVersion() is not None: + self.add_version = False + self.config_text = "%s, firmware version 1.%d" % (self.config_text, Hardware.GetFirmwareVersion()) + self.main_frame.SetConfigText(self.config_text) + if not self.band_up_down: + # Poll the hardware for changed frequency. This is used for hardware + # that can change its frequency independently of Quisk; eg. K3. + tune, vfo = Hardware.ReturnFrequency() + if tune is not None and vfo is not None: + self.BandFromFreq(tune) + self.ChangeDisplayFrequency(tune - vfo, vfo) + self.FldigiPoll() + self.HamlibPoll() + #if self.timer - self.fewsec_time0 > 3.0: + # self.fewsec_time0 = self.timer + # print ('fewswc') + if self.timer - self.save_time0 > 20.0: + self.save_time0 = self.timer + if self.CheckState(): + self.SaveState() + self.local_conf.SaveState() + if self.tmp_playing and QS.set_record_state(-1): # poll to see if playback is finished + self.btnTmpPlay.SetValue(False, True) + if self.file_play_state == 0: + pass + elif self.file_play_state == 1: + if QS.set_record_state(-1): # poll to see if playback is finished + if self.file_play_source == 12 and self.file_play_repeat: # repeat the CW message + self.file_play_state = 2 # Waiting for the timer to expire, and start another playback + self.file_play_timer = self.timer + self.file_play_repeat + self.SetPTT(False) + else: + self.btnFilePlay.SetValue(False, True) + elif self.file_play_state == 2: + if self.timer >= self.file_play_timer: + QS.set_record_state(5) # Start another playback + self.file_play_state = 1 + self.SetPTT(True) + def OnReadMIDI(self, byts): + for byt in byts: + if byt & 0x80: # this is a status byte and the start of a new message + self.midi_message = [byt] + else: + self.midi_message.append(byt) + if len(self.midi_message) == 3: + if self.config_midi_window: + self.config_midi_window.OnNewMidiNote(self.midi_message) + if self.midi_handler is None: + if hasattr(conf, 'MidiHandler'): + self.midi_handler = conf.MidiHandler(self, conf) + else: + import midi_handler + self.midi_handler = midi_handler.MidiHandler(self, conf) + self.midi_handler.OnReadMIDI(byts) + +def main(): + """If quisk is installed as a package, you can run it with quisk.main().""" + App() + application.MainLoop() + +if __name__ == '__main__': + main() + diff --git a/quisk_conf_defaults.py b/quisk_conf_defaults.py new file mode 100644 index 0000000..eed6277 --- /dev/null +++ b/quisk_conf_defaults.py @@ -0,0 +1,2389 @@ +from __future__ import absolute_import +from __future__ import division +# ** This is the file quisk_conf_defaults.py which contains defaults for Quisk. ** +# +# NOTE: You probably do not want to use a configuration file. Configuration files are obsolete +# because almost all configuration is done from the configuration screens in Quisk. +# +# Please do not change this configuration file quisk_conf_defaults.py. +# Instead copy one of the other quisk_conf_*.py files to your own +# configuration file and make changes there. +# +# For Linux, your standard configuration file name is .quisk_conf.py in your home directory. +# +# For Windows, your standard comfiguration file name is quisk_conf.py in your Documents folder. +# +# You can specify a different configuration file with the -c or --config command line argument. +# +# Check the config screen to make sure that the correct configuration file is in use. +# +# +# PLEASE DO **NOT** COPY THIS FILE AND USE IT AS A START FOR YOUR CONFIGURATION FILE! +# YOUR CONFIGURATION FILE SHOULD ONLY HAVE LINES THAT DIFFER FROM THIS FILE. QUISK +# IMPORTS THIS FILE FIRST, AND THEN YOUR CONFIG FILE OVERWRITES A FEW ITEMS SUCH AS +# SOUND CARD NAMES. +# +# Quisk imports quisk_conf_defaults.py to set its configuration. +# If you have a configuration file, it then overwrites the defaults +# with your parameters. +# +# Quisk uses a hardware file to control your transceiver and optionally other station hardware. +# Your config file specifies the hardware file to use. Quisk comes with several hardware +# files, and you can write your own hardware file in Python to do anything you want. +# +# Quisk has a custom decimation scheme for each sample rate. The allowable sample rates +# are the four SDR-IQ rates plus 24, 48, 96, 192, 240, 384, 480, and 960 ksps. Other rates +# can be added. + +import sys +import wx + +# Import the default Hardware module. You can import a different module in +# your configuration file. +import quisk_hardware_model as quisk_hardware + +# Module for additional widgets (advanced usage). See n2adr/quisk_widgets.py for an example. +# import n2adr.quisk_widgets as quisk_widgets +quisk_widgets = None + + + +################ Receivers SoftRock USB, Devices controlled by USB that capture samples from a sound card, and (for Tx) play samples to a sound card +## hardware_file_name Hardware file path, rfile +# This is the file that contains the control logic for each radio. +#hardware_file_name = 'softrock/hardware_usb.py' + +## widgets_file_name Widget file path, rfile +# This optional file adds additional controls for the radio. +#widgets_file_name = 'softrock/widgets_tx.py' + +use_sdriq = 0 # Get ADC samples from SDR-IQ is not used +use_rx_udp = 0 # Get ADC samples from UDP is not used +use_soapy = 0 # Get ADC samples from SoapySDR is not used +sample_rate = 48000 # name_of_sound_capt hardware sample rate in Hertz +if sys.platform == "win32": + name_of_sound_capt = "Primary" + name_of_sound_play = "Primary" +elif sys.platform == "darwin": + name_of_sound_capt = "pulse" + name_of_sound_play = "pulse" +else: + name_of_sound_capt = "hw:0" # Name of soundcard capture hardware device. + name_of_sound_play = "hw:0" +channel_i = 0 # Soundcard index of in-phase channel: 0, 1, 2, ... +channel_q = 1 # Soundcard index of quadrature channel: 0, 1, 2, ... + +## usb_vendor_id Vendor ID for USB control, integer +# USB devices have a vendor ID and a product ID. +usb_vendor_id = 0x16c0 + +## usb_product_id Product ID for USB control, integer +# USB devices have a vendor ID and a product ID. +usb_product_id = 0x05dc + +# I2C-address of the Si570 in the softrock; Thanks to Joachim Schneider, DB6QS +## si570_i2c_address I2C address, integer +# I2C-address of the Si570 in the softrock. +si570_i2c_address = 0x55 +#si570_i2c_address = 0x70 + +# Thanks to Ethan Blanton, KB8OJH, for this patch for the Si570 (many SoftRock's): +## si570_direct_control Use Si570 direct control, boolean +# If you are using a DG8SAQ interface to set a Si570 clock directly, set +# this to True. Complex controllers which have their own internal +# crystal calibration do not require this. +si570_direct_control = False +#si570_direct_control = True + +## si570_xtal_freq Si570 crystal frequency, integer +# This is the Si570 startup frequency in Hz. 114.285MHz is the typical +# value from the data sheet; you can use 'usbsoftrock calibrate' to find +# the value for your device. +si570_xtal_freq = 114285000 + +## repeater_delay Repeater delay secs, number +# The fixed delay for changing the repeater Rx/Tx frequency in seconds. +repeater_delay = 0.25 + +## rx_max_amplitude_correct Max ampl correct, number +# If you get your I/Q samples from a sound card, you will need to correct the +# amplitude and phase for inaccuracies in the analog hardware. The correction is +# entered using the controls from the "Rx Phase" button on the config screen. +# You must enter a positive number. This controls the range of the control. +rx_max_amplitude_correct = 0.2 + +## rx_max_phase_correct Max phase correct, number +# If you get your I/Q samples from a sound card, you will need to correct the +# amplitude and phase for inaccuracies in the analog hardware. The correction is +# entered using the controls from the "Rx Phase" button on the config screen. +# You must enter a positive number. This controls the range of the control in degrees. +rx_max_phase_correct = 10.0 + +## tx_level Tx Level, dict +# This is the level of the Tx audio sent to SoftRock hardware after all processing as a percentage +# number from 0 to 100. +# The level should be below 100 to allow headroom for amplitude and phase adjustments. +# Changes are immediate (no need to restart). +tx_level = {} + +## digital_tx_level Digital Tx power %, integer +# Digital modes reduce power by the percentage on the config screen. +# This is the maximum value of the slider. +digital_tx_level = 100 + + + +################ Receivers SoftRock Fixed, Fixed frequency devices that capture samples from a sound card, and (for Tx) play samples to a sound card +## hardware_file_name Hardware file path, rfile +# This is the file that contains the control logic for each radio. +#hardware_file_name = 'quisk_hardware_fixed.py' + +## widgets_file_name Widget file path, rfile +# This optional file adds additional controls for the radio. +#widgets_file_name = '' + +## fixed_vfo_freq Fixed VFO frequency, integer +# The fixed VFO frequency. That is, the frequency in the center of the screen. +fixed_vfo_freq = 7056000 + +## rx_max_amplitude_correct Max ampl correct, number +# If you get your I/Q samples from a sound card, you will need to correct the +# amplitude and phase for inaccuracies in the analog hardware. The correction is +# entered using the controls from the "Rx Phase" button on the config screen. +# No correction is 1.00. This controls the range of the control. +rx_max_amplitude_correct = 0.2 + +## rx_max_phase_correct Max phase correct, number +# If you get your I/Q samples from a sound card, you will need to correct the +# amplitude and phase for inaccuracies in the analog hardware. The correction is +# entered using the controls from the "Rx Phase" button on the config screen. +# No correction is 0.00. This controls the range of the control in degrees. +rx_max_phase_correct = 10.0 + +## tx_level Tx Level, dict +# This is the level of the Tx audio sent to SoftRock hardware after all processing as a percentage +# number from 0 to 100. +# The level should be below 100 to allow headroom for amplitude and phase adjustments. +# Changes are immediate (no need to restart). +tx_level = {} + +## digital_tx_level Digital Tx power %, integer +# Digital modes reduce power by the percentage on the config screen. +# This is the maximum value of the slider. +digital_tx_level = 100 + + + +################ Receivers HiQSDR, The original N2ADR hardware and the improved HiQSDR using UDP +## hardware_file_name Hardware file path, rfile +# This is the file that contains the control logic for each radio. +#hardware_file_name = 'hiqsdr/quisk_hardware.py' + +## widgets_file_name Widget file path, rfile +# This optional file adds additional controls for the radio. +#widgets_file_name = '' + +# For the N2ADR 2010 transceiver described in QEX, and for the improved version HiQSDR, +# see the sample config file in the hiqsdr package directory, and set these: + +## use_rx_udp Hardware type, integer choice +# This is the type of UDP hardware. Use 1 for the original hardware by N2ADR. +# Use 2 for the HiQSDR. +#use_rx_udp = 2 +#use_rx_udp = 1 +#use_rx_udp = 17 + +## tx_level Tx Level, dict +# This sets the transmit level 0 to 255 for each band. +# The config screen has a slider 0 to 100% so you can reduce the transmit power. +# The hardware only supports a power adjustment range of 20 dB, so zero is still a small amount of power. +tx_level = {} + +## digital_tx_level Digital Tx power %, integer +# Digital modes reduce power by the percentage on the config screen. +# This is the maximum value of the slider. +digital_tx_level = 100 + +## HiQSDR_BandDict IO Bus, dict +# If you use the HiQSDR hardware, set these: +# The HiQSDR_BandDict sets the preselect (4 bits) on the X1 connector. +HiQSDR_BandDict = { + '160':1, '80':2, '40':3, '30':4, '20':5, '15':6, '17':7, + '12':8, '10':9, '6':10, '500k':11, '137k':12 } + +## cw_delay CW Delay, integer +# This is the delay for CW from 0 to 255. +cw_delay = 0 + +## rx_udp_ip IP address, text +# This is the IP address of your hardware. +# For FPGA firmware version 1.4 and newer, and if enabled, the hardware is set to the IP address you enter here. +# For older firmware, the IP address is programmed into the FPGA, and you must enter that address. +rx_udp_ip = "192.168.2.196" +#rx_udp_ip = "192.168.1.196" + +## rx_udp_port Hardware UDP port, integer +# This is the base UDP port number of your hardware. +rx_udp_port = 0xBC77 + +## rx_udp_ip_netmask Network netmask, text +# This is the netmask for the network. +rx_udp_ip_netmask = '255.255.255.0' + +## tx_ip Transmit IP, text +# Leave this blank to use the same IP address as the receive hardware. Otherwise, enter "disable" +# to disable sending transmit I/Q samples, or enter the actual IP address. You must enter "disable" +# if you have multiple hardwares on the network, and only one should transmit. +tx_ip = "" +#tx_ip = "disable" +#tx_ip = "192.168.1.201" + +## tx_audio_port Tx audio UDP port, integer +# This is the UDP port for transmit audio I/Q samples. Enter zero to calculate this from the +# base hardware port. Otherwise enter the special custom port. +tx_audio_port = 0 + +## rx_udp_clock Clock frequency Hertz, integer +# This is the clock frequency of the hardware in Hertz. +rx_udp_clock = 122880000 + +## sndp_active Enable setting IP, boolean +# If possible, set the IP address to the address entered. +# For FPGA firmware version 1.4 and newer, the hardware is set to the IP address you enter here. +# For older firmware, the IP address is programmed into the FPGA, and you must enter that address. +sndp_active = True +#sndp_active = False + + + + + +################ Receivers Hermes, The Hermes-Lite Project and possibly other hardware with the Hermes FPGA code. +## hardware_file_name Hardware file path, rfile +# This is the file that contains the control logic for each radio. +#hardware_file_name = 'hermes/quisk_hardware.py' + +## widgets_file_name Widget file path, rfile +# This optional file adds additional controls for the radio. +#widgets_file_name = 'hermes/quisk_widgets.py' + +# Quisk has support for the Hermes-Lite project. This support will be extended to the original Hermes. +# Use the file hermes/quisk_conf.py as a model config file. The Hermes can obtain its IP address from +# DHCP. Set rx_udp_ip to the null string in this case. Or use rx_udp_ip to specify an IP address, but +# be sure it is unique and not in use by a DHCP server. The tx_ip and tx_audio_port are not used. +# Note: Setting the IP fails for the Hermes-Lite. +# You can set these options: + +## use_rx_udp Hardware type, integer choice +# This is the type of UDP hardware. Use 10 for the Hermes protocol. +#use_rx_udp = 10 + +## udp_rx_ip Hermes known IP, text +# Leave this blank to find the Hermes hardware with the usual UDP broadcast method. +# But this will not work on a VPN or when broadcasts are not routed +# to remote networks. In that case, enter the known IP address of the Hermes hardware. +udp_rx_ip = '' +#udp_rx_ip = '192.168.1.86' + +## rx_udp_port Hardware UDP port, integer +# This is the UDP port number of your hardware. +#rx_udp_port = 1024 + +## rx_udp_ip IP change, text +# This item should be left blank. It is used to change the IP address of the hardware to a different +# IP once the hardware is found by the broadcast method. Not all Hermes firmware supports changing the IP address. +#rx_udp_ip = "" + +## tx_ip Transmit IP, text +# Leave this blank to use the same IP address as the receive hardware. Otherwise, enter "disable" +# to disable sending transmit I/Q samples, or enter the actual IP address. You must enter "disable" +# if you have multiple hardwares on the network, and only one should transmit. This item is normally blank. +tx_ip = "" +#tx_ip = "disable" + +## tx_audio_port Tx audio UDP port, integer +# This is the UDP port for transmit audio I/Q samples. Enter zero to calculate this from the +# base hardware port. Otherwise enter the special custom port. +tx_audio_port = 0 + +## rx_udp_clock Clock frequency Hertz, integer +# This is the clock frequency of the hardware in Hertz. For HermesLite ver2 use 76800000. +#rx_udp_clock = 73728000 +#rx_udp_clock = 61440000 +#rx_udp_clock = 76800000 + +## tx_level Tx Level, dict +# This sets the transmit level 0 to 255 for each band. +# The config screen has a slider 0 to 100% so you can reduce the transmit power. +# The hardware only supports a limited adjustment range, so zero is still a small amount of power. +# Changes are immediate (no need to restart). +tx_level = {} + +## digital_tx_level Digital Tx power %, integer +# Digital modes reduce power by the percentage on the config screen. +# This is the maximum value of the slider. +#digital_tx_level = 100 + + +## hermes_code_version Hermes code version, integer +# There can be multiple Hermes devices on a network, but Quisk can only use one of these. If you have multiple +# hermes devices, you can use this to specify a unique device. Or use -1 to accept any board. +hermes_code_version = -1 + +## hermes_board_id Hermes board ID, integer +# There can be multiple Hermes devices on a network, but Quisk can only use one of these. If you have multiple +# hermes devices, you can use this to specify a unique device. Or use -1 to accept any board. +hermes_board_id = -1 + +## hermes_lowpwr_tr_enable Disable T/R in low power, boolean +# This option only applies to the Hermes Lite 2. +# This has no effect if the power amp is on. If the power amp is off and this setting is True, +# then the T/R relay does not switch but remains in the Rx state. You then have an RF output at RF1 +# and also a separate receive input at the main antenna connector RF2. +# Changes are immediate (no need to restart). +hermes_lowpwr_tr_enable = False +#hermes_lowpwr_tr_enable = True + +## hermes_power_amp Enable power amp, boolean +# This option only applies to the Hermes Lite 2. +# When True, the power amp is turned on and RF is sent to the main antenna connector RF2. +# Otherwise, low power RF is sent to RF1. +# Changes are immediate (no need to restart). +hermes_power_amp = False +#hermes_power_amp = True + +## power_meter_calib_name Power meter calibration, text choice +# This is the calibration table used to convert the power sensor voltage measured by the ADC to the transmit power display. +# It is a table of ADC codes and the corresponding measured power level. If you have a power meter, you can create your own +# table by selecting "New". Then enter ten or more power measurements from low to full power. +# For the Hermes-Lite version E3 filter board, use the built-in table "HL2FilterE3". +# Changes are immediate (no need to restart). +power_meter_calib_name = 'HL2FilterE3' + +## hermes_disable_sync Disable Power Supply Sync, boolean +# This option only applies to the Hermes Lite 2. +# When True, the FPGA will not generate a switching frequency for the power supply to +# move the harmonics out of amateur bands. +# Changes are immediate (no need to restart). +hermes_disable_sync = False +#hermes_disable_sync = True + +## Hware_Hl2_EepromIP Eeprom IP Address, text +# This is the IP address stored in the Hermes Lite EEPROM. It is only used at power on. If you set an address here +# be sure to write it down. To use this address you must set "Eeprom IP Usage". +# And you may want to set "Hermes known IP" too. Make sure the address does not conflict +# with your DHCP server. +Hware_Hl2_EepromIP = '192.168.1.6' +#Hware_Hl2_EepromIP = '192.168.1.241' + +## Hware_Hl2_EepromIPUse Eeprom IP Usage, text choice +# This is the way the EEPROM IP address is used at power on. +# "Ignore" means it is not used at all. "Set address" means DHCP is not used and the EEPROM IP address is used. +# "Use DHCP first" means the EEPROM IP address is only used if DHCP fails. +Hware_Hl2_EepromIPUse = 'Ignore' +#Hware_Hl2_EepromIPUse = 'Use DHCP first' +#Hware_Hl2_EepromIPUse = 'Set address' + +## Hware_Hl2_EepromMAC Eeprom MAC Address, text +# When you start Quisk, this shows the last two bytes of the MAC address that is stored in the EEPROM. +# If you change this item, Quisk writes the last two bytes into EEPROM. The HL2 can use this MAC address +# at power on instead of the factory supplied MAC address 0:1c:c0:a2:13:dd. It is only used at power on, +# and it is only used if it is marked valid by setting "Eeprom MAC usage" to "Set address". +# If you have two HL2's, you must have a unique MAC address for each. +Hware_Hl2_EepromMAC = '0xA1 0x6B' +#Hware_Hl2_EepromMAC = '0x4C 0x33' + +## Hware_Hl2_EepromMACUse Eeprom MAC Usage, text choice +# When you start Quisk, this shows the "Valid MAC bytes" bit in the HL2. If it is "Set address" (bit is 1) +# the HL2 will use the last two MAC bytes from "Eeprom MAC address"; otherwise the default MAC is used. +# If you change this item, the bit is written to the HL2. +# The HL2 only uses these bits at power on. +Hware_Hl2_EepromMACUse = 'Ignore' +#Hware_Hl2_EepromMACUse = 'Set address' + +## hermes_TxLNA_dB LNA during Tx dB, integer +# During transmit the low noise Rx amplifier gain changes to this value (in dB) if the hardware supports it. +# Changes are immediate (no need to restart). +hermes_TxLNA_dB = -12 + +## hermes_tx_buffer_latency Tx buffer msec, integer +# This is the latency of the transmit buffer in milliseconds. +hermes_tx_buffer_latency = 10 +#hermes_tx_buffer_latency = 20 + +## hermes_PTT_hang_time PTT hang time msec, integer +# This is the hang time for hardware push-to-talk in milliseconds. +hermes_PTT_hang_time = 4 +#hermes_PTT_hang_time = 15 + +## hermes_antenna_tuner Antenna tuner, text choice +# This option only applies to the Hermes Lite 2. Set this to None if you don't have a tuner. +# Set this to "Tune" to control the Icom AH-4 compatible ATU attached to the Hermes Lite 2 end plate. +# Then when the Spot button is pressed with a positive power, a tune request is sent to the ATU. +# If the Spot button is pressed with a zero power level, the tuner is set to bypass mode. +# Changes are immediate (no need to restart). +hermes_antenna_tuner = 'None' +#hermes_antenna_tuner = 'Tune' + +## hermes_PWM Use PWM volts, text choice +# Hermes-Lite2 hardware has a PWM variable voltage source. This voltage can +# be used to control external devices. If you have not connected this voltage, this setting does not matter. +# Choose "Fan speed" to use this voltage to control the fan speed. +# Choose "Band indicator" to use this voltage to indicate the band in use. +# Changes are immediate (no need to restart). +hermes_PWM = "Fan speed" +#hermes_PWM = "Band indicator" + +## hermes_disable_watchdog Disable watchdog, boolean +# Hermes-Lite2 hardware has a watchdog timer that requires the host computer to send regular +# commands to keep the HL2 running. This ensures that the HL2 doesn't continue sending data +# if the host computer program crashes. This disables the watchdog. +# This is normally False. +# Changes are immediate (no need to restart). +hermes_disable_watchdog = False +#hermes_disable_watchdog = True + +## hermes_reset_on_disconnect Reset on disconnect, boolean +# This will cause the Hermes-Lite2 to reset on each disconnect from the host. +# This is normally False. +# Changes are immediate (no need to restart). +hermes_reset_on_disconnect = False +#hermes_reset_on_disconnect = True + +## hermes_bias_adjust Enable bias adjust, boolean +# This option only applies to the Hermes Lite 2. +# Below are controls that adjust the bias on the power output transistors. Before you enable adjustment, +# make sure you know the correct drain current and how to monitor the current. +# Then set this to True. When you are finished, set it back to False. The bias adjustment +# is stored in the hardware only when the "Write" button is pressed. +# Changes are immediate (no need to restart). +hermes_bias_adjust = False +#hermes_bias_adjust = True + +## hermes_iob_rxin IO board Rx input, text choice +# This controls the usage of the Rx input J9 and the Pure Signal input J10 on the N2ADR IO board. This option +# has no effect if the IO board is not installed. +# The first option disables the Rx input at J9 and the HL2 operates as usual. The Pure Signal input at J10 is available. +# The second option connects the HL2 receive input to J9. J10 is not available. +# The third option connects the HL2 receive input to J9 on Rx and to J10 on Tx. +# Changes are immediate (no need to restart). +hermes_iob_rxin = 'J10 available' +#hermes_iob_rxin = 'HL2 Rx to J9' +#hermes_iob_rxin = 'Use J9 and J10' + + +# These are known power meter calibration tables. This table is not present in the JSON settings file. +power_meter_std_calibrations = {} +power_meter_std_calibrations['HL2FilterE3'] = [[ 0, 0.0 ], [ 25.865384615384617, 0.0025502539351328003 ], [ 101.02453987730061, 0.012752044999999998 ], + [ 265.2901234567901, 0.050600930690879994 ], [ 647.9155844155844, 0.21645831264800003 ], [ 1196.5935483870967, 0.66548046472992 ], + [ 1603.7032258064517, 1.1557229391679997 ], [ 2012.3271604938273, 1.811892166688 ], [ 2616.7727272727275, 3.0085848760319993 ], + [ 3173.818181818182, 4.3927428485119995 ], [ 3382.7922077922076, 4.9791328857920005 ], [ 3721.0714285714284, 6.024750791808321 ], + [ 4093.1785714285716, 7.28994845808807 ], [ 4502.496428571429, 8.820837634286566 ], [ 4952.746071428572, 10.673213537486745 ] ] +#power_meter_std_calibrations['HL2FilterE1'] = [[0, 0.0], [9.07, 0.002], [54.98, 0.014], [148.6, 0.057], +# [328.0, 0.208], [611.1, 0.646], [807.0, 1.098], [982.1, 1.6], [1223.3, 2.471], [1517.7, 3.738], [1758.7, 5.02]] + +## Hermes_BandDict Rx IO Bus, dict +# The Hermes_BandDict sets the 7 bits on the J16 connector for Rx. +Hermes_BandDict = { + '160':0b0000001, '80':0b1000010, '60':0b1000100, '40':0b1000100, '30':0b1001000, '20':0b1001000, '17':0b1010000, + '15':0b1010000, '12':0b1100000, '10':0b1100000} + +## Hermes_BandDictTx Tx IO Bus, dict +# The Hermes_BandDictTx sets the 7 bits on the J16 connector for Tx if enabled. +Hermes_BandDictTx = {'160':0, '80':0, '60':0, '40':0, '30':0, '20':0, '17':0, '15':0, '12':0, '10':0} + +## Hermes_BandDictEnTx Enable Tx Filt, boolean +# Enable the separate Rx and Tx settings for the J16 connector. +Hermes_BandDictEnTx = False +#Hermes_BandDictEnTx = True + + +################ Receivers Red Pitaya, The Red Pitaya Project by Pavel Demin. This uses the Hermes FPGA code. +## hardware_file_name Hardware file path, rfile +# This is the file that contains the control logic for each radio. +#hardware_file_name = 'hermes/quisk_hardware.py' + +## widgets_file_name Widget file path, rfile +# This optional file adds additional controls for the radio. +#widgets_file_name = '' + +## use_rx_udp Hardware type, integer choice +# This is the type of UDP hardware. Use 10 for the Hermes protocol. +#use_rx_udp = 10 + +## rx_udp_ip IP change, text +# This item should be left blank. It is used to change the IP address of the hardware to a different +# IP once the hardware is found. Not all Hermes firmware supports changing the IP address. +#rx_udp_ip = "" + +## rx_udp_port Hardware UDP port, integer +# This is the UDP port number of your hardware. +#rx_udp_port = 1024 + +## rx_udp_ip_netmask Network netmask, text +# This is the netmask for the network. +#rx_udp_ip_netmask = '255.255.255.0' + +## tx_ip Transmit IP, text +# Leave this blank to use the same IP address as the receive hardware. Otherwise, enter "disable" +# to disable sending transmit I/Q samples, or enter the actual IP address. You must enter "disable" +# if you have multiple hardwares on the network, and only one should transmit. This item is normally blank. +tx_ip = "" +#tx_ip = "disable" + +## tx_audio_port Tx audio UDP port, integer +# This is the UDP port for transmit audio I/Q samples. Enter zero to calculate this from the +# base hardware port. Otherwise enter the special custom port. +tx_audio_port = 0 + +## rx_udp_clock Clock frequency Hertz, integer +# This is the clock frequency of the hardware in Hertz. +#rx_udp_clock = 125000000 + +## tx_level Tx Level, dict +# This sets the transmit level 0 to 255 for each band. +# The config screen has a slider 0 to 100% so you can reduce the transmit power. +# The hardware only supports a limited adjustment range, so zero is still a small amount of power. +tx_level = {} + +## digital_tx_level Digital Tx power %, integer +# Digital modes reduce power by the percentage on the config screen. +# This is the maximum value of the slider. +#digital_tx_level = 100 + +## hermes_code_version Hermes code version, integer +# There can be multiple Hermes devices on a network, but Quisk can only use one of these. If you have multiple +# hermes devices, you can use this to specify a unique device. Or use -1 to accept any board. +hermes_code_version = -1 + +## hermes_board_id Hermes board ID, integer +# There can be multiple Hermes devices on a network, but Quisk can only use one of these. If you have multiple +# hermes devices, you can use this to specify a unique device. Or use -1 to accept any board. +hermes_board_id = -1 + +## Hermes_BandDict Hermes Bus, dict +# The Hermes_BandDict sets the 7 bits on the J16 connector. +Hermes_BandDict = { + '160':0b0000001, '80':0b0000010, '60':0b0000100, '40':0b0001000, '30':0b0010000, '20':0b0100000, '15':0b1000000} + +## Hermes_BandDictTx Tx IO Bus, dict +# The Hermes_BandDictTx sets the 7 bits on the J16 connector for Tx if enabled. +Hermes_BandDictTx = {'160':0, '80':0, '60':0, '40':0, '30':0, '20':0, '17':0, '15':0, '12':0, '10':0} + +## Hermes_BandDictEnTx Enable Tx Filt, boolean +# Enable the separate Rx and Tx settings for the J16 connector. +Hermes_BandDictEnTx = False +#Hermes_BandDictEnTx = True + + +################ Receivers SoapySDR, The SoapySDR interface to multiple hardware SDRs. +## hardware_file_name Hardware file path, rfile +# This is the file that contains the control logic for each radio. +#hardware_file_name = 'soapypkg/quisk_hardware.py' + +## widgets_file_name Widget file path, rfile +# This optional file adds additional controls for the radio. +#widgets_file_name = '' + +## use_soapy Use SoapySDR, integer +# Enter 1 to turn on SoapySDR. +#use_soapy = 1 + +# Further items are present in the radio dictionary with names soapy_* + + +################ Receivers SdrIQ, The SDR-IQ receiver by RfSpace +## hardware_file_name Hardware file path, rfile +# This is the file that contains the control logic for each radio. +hardware_file_name = 'quisk_hardware_sdriq.py' +#hardware_file_name = 'sdriqpkg/quisk_hardware.py' + +## widgets_file_name Widget file path, rfile +# This optional file adds additional controls for the radio. +#widgets_file_name = '' + +# +# For the SDR-IQ the soundcard is not used for capture. + +sdriq_decimation = 1250 + +## use_sdriq Hardware by RF-Space, integer choice +# This is the type of hardware. For the SdrIQ, use_sdriq is 1. +#use_sdriq = 1 + +## sdriq_name Serial port, text +# The name of the SDR-IQ serial port to open. +#sdriq_name = "/dev/ttyUSB0" +#sdriq_name = "COM6" +#sdriq_name = "/dev/ft2450" + +## sdriq_clock Clock frequency Hertz, number +# This is the clock frequency of the hardware in Hertz. +#sdriq_clock = 66666667.0 + + + +################ Receivers Odyssey, The Odyssey project using a UDP protocol similar to the HiQSDR +## hardware_file_name Hardware file path, rfile +# This is the file that contains the control logic for each radio. +#hardware_file_name = 'hiqsdr/quisk_hardware.py' + +## widgets_file_name Widget file path, rfile +# This optional file adds additional controls for the radio. +#widgets_file_name = '' + +## use_rx_udp Hardware type, integer choice +# This is the type of UDP hardware. The Odyssey uses type 2. +#use_rx_udp = 2 + +## tx_level Tx Level, dict +# This sets the transmit level 0 to 255 for each band. +# The config screen has a slider 0 to 100% so you can reduce the transmit power. +# The hardware only supports a limited adjustment range, so zero is still a small amount of power. +tx_level = {} + +## digital_tx_level Digital Tx power %, integer +# Digital modes reduce power by the percentage on the config screen. +# This is the maximum value of the slider. +digital_tx_level = 100 + +## HiQSDR_BandDict IO Bus, dict +# This sets the preselect (4 bits) on the X1 connector. +HiQSDR_BandDict = { + '160':1, '80':2, '40':3, '30':4, '20':5, '15':6, '17':7, + '12':8, '10':9, '6':10, '500k':11, '137k':12 } + +## cw_delay CW Delay, integer +# This is the delay for CW from 0 to 255. +cw_delay = 0 + +## rx_udp_ip IP address, text +# This is the IP address of your hardware. +# For FPGA firmware version 1.4 and newer, and if enabled, the hardware is set to the IP address you enter here. +# For older firmware, the IP address is programmed into the FPGA, and you must enter that address. +rx_udp_ip = "192.168.2.160" +#rx_udp_ip = "192.168.1.196" + +## rx_udp_port Hardware UDP port, integer +# This is the UDP port number of your hardware. +rx_udp_port = 48247 + +## rx_udp_ip_netmask Network netmask, text +# This is the netmask for the network. +rx_udp_ip_netmask = '255.255.255.0' + +## tx_ip Transmit IP, text +# Leave this blank to use the same IP address as the receive hardware. Otherwise, enter "disable" +# to disable sending transmit I/Q samples, or enter the actual IP address. You must enter "disable" +# if you have multiple hardwares on the network, and only one should transmit. +tx_ip = "" +#tx_ip = "disable" +#tx_ip = "192.168.1.201" + +## tx_audio_port Tx audio UDP port, integer +# This is the UDP port for transmit audio I/Q samples. Enter zero to calculate this from the +# base hardware port. Otherwise enter the special custom port. +tx_audio_port = 0 + +## rx_udp_clock Clock frequency Hertz, integer +# This is the clock frequency of the hardware in Hertz. +rx_udp_clock = 122880000 + +## sndp_active Enable setting IP, boolean +# If possible, set the IP address to the address entered. +# For FPGA firmware version 1.4 and newer, the hardware is set to the IP address you enter here. +# For older firmware, the IP address is programmed into the FPGA, and you must enter that address. +sndp_active = True +#sndp_active = False + +## radio_sound_ip IP sound play, text +# This option sends radio playback sound to a UDP device. Some SDR hardware devices have an +# audio codec that can play radio sound with less latency than a soundcard. The sample rate +# is the same as the soundcard sample rate, but probably you will want 48000 sps. The UDP +# data consists of two bytes of zero, followed by the specified number of samples. Each +# sample consists of two bytes (a short) of I data and two bytes of Q data in little-endian order. +# For radio_sound_nsamples = 360, the total number of UDP data bytes is 1442. +#radio_sound_ip = "192.168.2.160" + +## radio_sound_port UDP port play, integer +# The UDP port of the radio sound play device. +#radio_sound_port = 48250 + +## radio_sound_nsamples Num play samples, integer +# The number of play samples per UDP block. +#radio_sound_nsamples = 360 + +## radio_sound_mic_ip IP microphone, text +# This option receives microphone samples from a UDP device. The UDP +# data consists of two bytes of zero, followed by the specified number of samples. Each +# sample consists of two bytes (a short) of monophonic microphone data in little-endian order. +# For radio_sound_mic_nsamples = 720, the total number of UDP data bytes is 1442. +#radio_sound_mic_ip = "192.168.2.160" + +## radio_sound_mic_port UDP port mic, integer +# The UDP port of the microphone device. +#radio_sound_mic_port = 48251 + +## radio_sound_mic_nsamples Num mic samples, integer +# The number of mic samples per UDP block. +#radio_sound_mic_nsamples = 720 + +## radio_sound_mic_boost Mic boost, boolean +# Use False for no microphone boost, or True for +20 dB boost. +#radio_sound_mic_boost = False +#radio_sound_mic_boost = True + + +################ Receivers Odyssey2, The Odyssey-2 project using the HPSDR Hermes protocol +## hardware_file_name Hardware file path, rfile +# This is the file that contains the control logic for each radio. +#hardware_file_name = 'hermes/quisk_hardware.py' + +## widgets_file_name Widget file path, rfile +# This optional file adds additional controls for the radio. +#widgets_file_name = 'hermes/quisk_widgets.py' + +# Use the file hermes/quisk_conf.py as a model config file. The Hermes can obtain its IP address from +# DHCP. Set rx_udp_ip to the null string in this case. Or use rx_udp_ip to specify an IP address, but +# be sure it is unique and not in use by a DHCP server. +# You can set these options: + +## use_rx_udp Hardware type, integer choice +# This is the type of UDP hardware. Use 10 for the Hermes protocol. +#use_rx_udp = 10 + +## rx_udp_ip IP change, text +# This item should be left blank. It is used to change the IP address of the hardware to a different +# IP once the hardware is found. Not all Hermes firmware supports changing the IP address. +#rx_udp_ip = "" + +## rx_udp_port Hardware UDP port, integer +# This is the UDP port number of your hardware. +#rx_udp_port = 1024 + +## rx_udp_ip_netmask Network netmask, text +# This is the netmask for the network. +#rx_udp_ip_netmask = '255.255.255.0' + +## tx_ip Transmit IP, text +# Leave this blank to use the same IP address as the receive hardware. Otherwise, enter "disable" +# to disable sending transmit I/Q samples, or enter the actual IP address. You must enter "disable" +# if you have multiple hardwares on the network, and only one should transmit. This item is normally blank. +tx_ip = "" +#tx_ip = "disable" + +## tx_audio_port Tx audio UDP port, integer +# This is the UDP port for transmit audio I/Q samples. Enter zero to calculate this from the +# base hardware port. Otherwise enter the special custom port. +tx_audio_port = 0 + +## rx_udp_clock Clock frequency Hertz, integer +# This is the clock frequency of the hardware in Hertz. For Odyssey use 122880000. +#rx_udp_clock = 122880000 + +## tx_level Tx Level, dict +# This sets the transmit level 0 to 255 for each band. +# The config screen has a slider 0 to 100% so you can reduce the transmit power. +# The hardware only supports a limited adjustment range, so zero is still a small amount of power. +tx_level = {} + +## digital_tx_level Digital Tx power %, integer +# Digital modes reduce power by the percentage on the config screen. +# This is the maximum value of the slider. +#digital_tx_level = 100 + + +## hermes_code_version Hermes code version, integer +# There can be multiple Hermes devices on a network, but Quisk can only use one of these. If you have multiple +# Hermes devices, you can use this to specify a unique device. Or use -1 to accept any board. +hermes_code_version = -1 + +## hermes_board_id Hermes board ID, integer +# There can be multiple Hermes devices on a network, but Quisk can only use one of these. If you have multiple +# Hermes devices, you can use this to specify a unique device. Or use -1 to accept any board. +hermes_board_id = -1 + +## Hermes_BandDict Hermes Bus, dict +# The Hermes_BandDict sets the 7 bits on the J16 connector. +Hermes_BandDict = { + '160':0b0000001, '80':0b0000010, '60':0b0000100, '40':0b0001000, '30':0b0010000, '20':0b0100000, '15':0b1000000} + +## Hermes_BandDictTx Tx IO Bus, dict +# The Hermes_BandDictTx sets the 7 bits on the J16 connector for Tx if enabled. +Hermes_BandDictTx = {'160':0, '80':0, '60':0, '40':0, '30':0, '20':0, '17':0, '15':0, '12':0, '10':0} + +## Hermes_BandDictEnTx Enable Tx Filt, boolean +# Enable the separate Rx and Tx settings for the J16 connector. +Hermes_BandDictEnTx = False +#Hermes_BandDictEnTx = True + + +################ Receivers Afedri, The Afedri SDR receiver with the Ethernet interface. +## hardware_file_name Hardware file path, rfile +# This is the file that contains the control logic for each radio. +#hardware_file_name = 'afedrinet/quisk_hardware.py' + +## widgets_file_name Widget file path, rfile +# This optional file adds additional controls for the radio. +#widgets_file_name = '' + +## rx_udp_ip IP address, text +# This is the IP address of your hardware. Enter 0.0.0.0 to search for the address. +#rx_udp_ip = "0.0.0.0" +#rx_udp_ip = "192.168.0.200" +#rx_udp_ip = "192.168.1.196" + +## rx_udp_port Hardware UDP port, integer +# This is the base UDP port number of your hardware. +#rx_udp_port = 50000 + +## rx_udp_ip_netmask Network netmask, text +# This is the netmask for the network. +#rx_udp_ip_netmask = '255.255.255.0' + +## rx_udp_clock Clock frequency Hertz, integer +# This is the clock frequency of the hardware in Hertz. +#rx_udp_clock = 80000000 + +## default_rf_gain Default RF gain, integer +# This is the RF gain when starting. +#default_rf_gain = 11 + + +################ Receivers Control Head, Use Quisk to control a remote radio. No real hardware is used here. +## hardware_file_name Hardware file path, rfile +# Quisk can be used as a control head to control a real radio located remotely. +# This file contains the control logic for remote radios of type Hermes, SoftRock or HiQSDR. +hardware_file_name = 'ac2yd/control_softrock.py' +#hardware_file_name = 'ac2yd/control_hermes.py' +#hardware_file_name = 'ac2yd/control_hiqsdr.py' + +## widgets_file_name Widget file path, rfile +# This file adds additional controls for the radio. Choose the correct file (if any) for the remote radio. +widgets_file_name = '' +#widgets_file_name = 'hermes/quisk_widgets.py' + + +################ Sound +# Playback devices: +# name_of_sound_play Play radio sound on speakers or headphones +# playback_rate The sample rate, normally 48000, 96000 or 192000 +# name_of_mic_play For sound card modes (like SoftRock), play I/Q transmit audio +# mic_playback_rate The sample rate +# mic_play_chan_I Channel number 0, 1, ... for I samples +# mic_play_chan_Q Channel number 0, 1, ... for Q samples +# tx_channel_delay Channel number for delay, or -1 +# digital_output_name Output monophonic digital samples to another program +# sample_playback_name Output digital I/Q samples to another program +# digital_rx1_name Output monophonic digital samples from Rx1 to another program +# Capture devices: +# microphone_name The monophonic microphone source +# mic_sample_rate The sample rate; must be 48000 +# mic_channel_I The channel number for samples +# mic_channel_Q Not used. +# name_of_sound_capt For sound card modes (like SoftRock), capture I/Q samples +# sample_rate The sample rate +# channel_i Channel number 0, 1, ... for I samples +# channel_q Channel number 0, 1, ... for Q samples +# channel_delay Channel number for delay, or -1 +# digital_input_name Receive monophonic digital samples from another program +# +# Unused devices have the null string "" as the name. For example, name_of_sound_play="" for a panadapter. +# +# On Linux, Quisk can access your sound card through ALSA, PortAudio or PulseAudio. +# On Windows, Quisk uses Wasapi for sound card access. + +## channel_i Sample channel I, integer +# Soundcard index of in-phase channel: 0, 1, 2, ... +channel_i = 0 +#channel_i = 1 + +## channel_q Sample channel Q, integer +# Soundcard index of quadrature channel: 0, 1, 2, ... +channel_q = 1 +#channel_q = 0 + +# Thanks to Franco Spinelli for this fix: +## channel_delay Rx channel delay, integer +# The H101 hardware using the PCM2904 chip has a one-sample delay between +# channels, which must be fixed in software. If you have this problem, +# change channel_delay to either channel_i or channel_q. Use -1 for no delay. +channel_delay = -1 +#channel_delay = 0 +#channel_delay = 1 + +## tx_channel_delay Tx channel delay, integer +# This is for mic playback (SoftRock transmit) +tx_channel_delay = -1 +#tx_channel_delay = 0 +#tx_channel_delay = 1 + +## playback_rate Playback rate, integer choice +# This is the received radio sound playback rate. The default will +# be 48 kHz for the SDR-IQ and UDP port samples, and sample_rate for sound +# card capture. Set it yourself for other rates or hardware. +# The playback_rate must be 24000, 48000, 96000 or 192000. +# The preferred rate is 48000 for use with digital modes and transmit of recorded audio. +#playback_rate = 48000 +#playback_rate = 24000 +#playback_rate = 96000 +#playback_rate = 192000 + +## lin_sample_playback_name Sample playback name, text +# This option sends the raw I/Q samples to another program using a loopback device (Linux) or +# a Virtual Audio Cable (Windows). The sample rate is the same as the hardware sample rate. +# Read the samples from the loopback device with another program. +lin_sample_playback_name = "" +#lin_sample_playback_name = "hw:Loopback,0" + +## win_sample_playback_name Sample playback name, text +# This option sends the raw I/Q samples to another program using a loopback device (Linux) or +# a Virtual Audio Cable (Windows). The sample rate is the same as the hardware sample rate. +# Read the samples from the loopback device with another program. +win_sample_playback_name = "" +#win_sample_playback_name = "COM6" + +sample_playback_name = "" + +# When you use the microphone input, the mic_channel_I and Q are the two capture +# microphone channels. Quisk uses a monophonic mic, so audio is taken from the I +# channel, and the Q channel is (currently) ignored. It is OK to set the same +# channel number for both, and this is necessary for a USB mono mic. The mic sample rate +# should be 48000 to enable digital modes and the sound recorder to work, but 8000 can be used. +# Mic samples can be sent to an Ethernet device (use tx_ip and name_of_mic_play = "") +# or to a sound card (use name_of_mic_play="hw:1" or other device). +# +# If mic samples are sent to a sound card for Tx, the samples are tuned to the audio +# transmit frequency, and are set to zero unless the key is down. You must set both +# microphone_name and name_of_mic_play even for CW. For softrock hardware, you usually +# capture radio samples and play Tx audio on one soundcard; and capture the mic and play radio +# sound on the other sound card at 48000 sps. For example: +# name_of_sound_capt = "hw:0" # high quality sound card at 48, 96, or 192 ksps +# name_of_sound_play = "hw:1" # lower quality sound card at 48 ksps +# microphone_name = name_of_sound_play +# name_of_mic_play = name_of_sound_capt + +## lin_name_of_sound_play Play radio sound, text +# Name of device to play demodulated radio audio. +lin_name_of_sound_play = "hw:0" + +## win_name_of_sound_play Play radio sound, text +# Name of device to play demodulated radio audio. +win_name_of_sound_play = "Primary" + +## lin_name_of_sound_capt Capture audio samples, text +# Name of device to capture samples from an audio device. +lin_name_of_sound_capt = "hw:0" + +## win_name_of_sound_capt Capture audio samples, text +# Name of device to capture samples from an audio device. +win_name_of_sound_capt = "Primary" + +## sample_rate Sample rate, integer +# The sample rate when capturing samples from a sound card. +#sample_rate = 48000 +#sample_rate = 96000 +#sample_rate = 192000 + +# Microphone capture: +## lin_microphone_name Microphone name, text +# Name of microphone capture device (or "hw:1") +lin_microphone_name = "" + +## win_microphone_name Microphone name, text +# Name of microphone capture device (or "hw:1") +win_microphone_name = "" + +microphone_name = '' + +## mic_sample_rate Mic sample rate, integer choice +# Microphone capture sample rate in Hertz, should be 48000, can be 8000 +mic_sample_rate = 48000 +#mic_sample_rate = 8000 + +## mic_channel_I Mic channel I, integer +# Soundcard index of mic capture audio channel +mic_channel_I = 0 + +## mic_channel_Q Mic channel Q, integer +# Soundcard index of ignored capture channel +mic_channel_Q = 0 + +## lin_name_of_mic_play Mic play name, text +# Tx audio samples sent to soundcard (SoftRock). +# Name of play device if Tx audio I/Q is sent to a sound card. +lin_name_of_mic_play = "" + +## win_name_of_mic_play Mic play name, text +# Tx audio samples sent to soundcard (SoftRock). +# Name of play device if Tx audio I/Q is sent to a sound card. +win_name_of_mic_play = "" + +name_of_mic_play = "" + +## mic_playback_rate Mic playback rate, integer +# Playback rate must be a multiple 1, 2, ... of mic_sample_rate +mic_playback_rate = 48000 +#mic_playback_rate = 24000 +#mic_playback_rate = 96000 +#mic_playback_rate = 192000 + +## mic_play_chan_I Mic play channel I, integer +# Soundcard index of Tx audio I play channel +mic_play_chan_I = 0 +#mic_play_chan_I = 1 + +## mic_play_chan_Q Mic play channel Q, integer +# Soundcard index of Tx audio Q play channel +mic_play_chan_Q = 1 +#mic_play_chan_Q = 0 + +## lin_digital_input_name Digital input name, text +# Input audio from an external program for use with digital modes. The input must be +# stereo at 48000 sps, and you must set mic_sample_rate to 48000 also. +lin_digital_input_name = "" + +## win_digital_input_name Digital input name, text +# Input audio from an external program for use with digital modes. The input must be +# stereo at 48000 sps, and you must set mic_sample_rate to 48000 also. +win_digital_input_name = "" + +digital_input_name = "" + +## lin_digital_output_name Digital output name, text +# Output audio to an external program for use with digital modes. The output is +# stereo at the same sample rate as the radio sound playback. +lin_digital_output_name = "" + +## win_digital_output_name Digital output name, text +# Output audio to an external program for use with digital modes. The output is +# stereo at the same sample rate as the radio sound playback. +win_digital_output_name = "" + +digital_output_name = "" + +## lin_digital_rx1_name Digital sub-receiver 1 output name, text +# Output audio to an external program for use with digital modes. +lin_digital_rx1_name = "" + +## win_digital_rx1_name Digital sub-receiver 1 output name, text +# Output audio to an external program for use with digital modes. +win_digital_rx1_name = "" + +digital_rx1_name = "" + +## digital_output_level Digital output level, number +# This is the volume control 0.0 to 1.0 for digital playback to fldigi, etc. +# Changes are immediate (no need to restart). +digital_output_level = 0.7 + +# Sound card names: +# +# In PortAudio, soundcards have an index number 0, 1, 2, ... and a name. +# The name can be something like "HDA NVidia: AD198x Analog (hw:0,0)" or +# "surround41". In Quisk, all PortAudio device names start with "portaudio". +# A device name like "portaudio#6" directly specifies the index. A name like +# "portaudio:text" means to search for "text" in all available devices. And +# there is a default device "portaudiodefault". So these portaudio names are useful: +#name_of_sound_capt = "portaudio:(hw:0,0)" # First sound card +#name_of_sound_capt = "portaudio:(hw:1,0)" # Second sound card, etc. +#name_of_sound_capt = "portaudio#1" # Directly specified index +#name_of_sound_capt = "portaudiodefault" # May give poor performance on capture +# +# In ALSA, soundcards have these names. The "hw" devices are the raw +# hardware devices, and should be used for soundcard capture. +#name_of_sound_capt = "hw:0" # First sound card +#name_of_sound_capt = "hw:1" # Second sound card, etc. +#name_of_sound_capt = "plughw" +#name_of_sound_capt = "plughw:1" +#name_of_sound_capt = "default" +# +# It is usually best to use ALSA names because they provide minimum latency. But +# you may need to use PulseAudio to connect to other programs such as wsjt-x. +# +# Pulseaudio support was added by Philip G. Lee. Many thanks! +# More pulse audio support was added by Eric Thornton, KM4DSJ. Many thanks! +# +# For PulseAudio devices, use the name "pulse:name" and connect the streams +# to your hardware devices using a PulseAudio control program. The name "pulse" +# alone refers to the "default" device. The PulseAudio names are quite long; +# for example "alsa_output.pci-0000_00_1b.0.analog-stereo". Look on the screen +# Config/Sound to see the device names. There is a description, a PulseAudio name, +# and for ALSA devices, the ALSA name. An example is: +# +# CM106 Like Sound Device Analog Stereo +# alsa_output.usb-0d8c_USB_Sound_Device-00-Device.analog-stereo +# USB Sound Device USB Audio (hw:1,0) +# +# Instead of the long PulseAudio name, you can enter a substring of any of +# these three strings. +# +# Use the default pulse device for radio sound: +#name_of_sound_play = "pulse" +# Use a PulseAudio name for radio sound: +#name_of_sound_play = "pulse:alsa_output.usb-0d8c_USB_Sound_Device-00-Device.analog-stereo" +# Abbreviate the PulseAudio name: +#name_of_sound_play = "pulse:alsa_output.usb" +# Another abbreviation: +#name_of_sound_play = "pulse:CM106" + +################ Options +## max_record_minutes Max minutes record time, number +# Quisk has record and playback buttons to save radio sound. If there is no more room for +# sound, the old sound is discarded and the most recent sound is retained. This controls +# the maximum time of sound storage in minutes for this recorded audio, and also the record +# time for the Tx Audio test screen. If you want to transmit recorded sound, then mic_sample_rate +# must equal playback_rate and both must be 48000. +max_record_minutes = 1.00 + +# Quisk can save radio sound and samples to files, and can play recorded sound. There is a button on the +# Config/Config screen to set the file names. You can set the initial names with these variables: +file_name_audio = "" +#file_name_audio = "/home/jim/tmp/qaudio.wav" + +file_name_samples = "" +#file_name_samples = "C:/tmp/qsamples.wav" +# The file for playback must be 48 ksps, 16-bit, one channel (monophonic); the same as the mic input. When +# you play a file, the PTT button (if any) is pushed. There is a control to repeat the playback. This +# feature is intended to transmit a "CQ CQ" message, for example, during a contest. +file_name_playback = "" +#file_name_playback = "/home/jim/sounds/cqcq_contest.wav" + +## do_repeater_offset Use repeater offset, boolean +# Quisk can implement the frequency shift needed for repeaters. If the repeater frequency +# is on the favorites screen, and you tune close (500 Hz) to that frequency in FM mode, +# then Quisk will shift the Tx frequency by the offset when transmitting. +# Quisk will also supply the CTCSS tone if one is entered. +# Note that no CTCSS tone is generated if no repeater offset is entered, but you can enter a small offset like 0.001. +# Your hardware file must define the method RepeaterOffset(self, offset=None). +do_repeater_offset = False +#do_repeater_offset = True + +## correct_smeter S-meter correction in S units, number +# This converts from dB to S-units for the S-meter (it is in S-units). +correct_smeter = 15.5 +#correct_smeter = 7.7 +#correct_smeter = 21.6 + +## agc_max_gain Maximum AGC gain, number +# There is a button to turn AGC on or off, +# but AGC still limits the peak amplitude to avoid clipping even if it is off. +# Right click the AGC button to show the adjustment slider. If the slider is at maximum, +# all signals will have the same (maximum) amplitude. For lower values, weak signals +# will be somewhat less loud than strong signals; that is, some variation in signal +# amplitude remains. +# agc_max_gain controls the maximum AGC gain and thus the scale of the AGC slider control. If +# it is too high, all signals reach the same amplitude at much less than 100% slider. +# If it is too low, then all signals fail to have the same amplitude even at 100%. But +# the value is not critical, because you can adjust the slider a bit more. +agc_max_gain = 15000.0 +#agc_max_gain = 10000.0 +#agc_max_gain = 20000.0 + +## agc_release_time AGC release time in seconds, number +# This is the AGC release time in seconds. It must be greater than zero. It is the time +# constant for gain recovery after a strong signal disappears. +agc_release_time = 1.0 +#agc_release_time = 2.0 +#agc_release_time = 0.5 + +## freq_spacing Frequency rounding spacing, integer +# If freq_spacing is not zero, frequencies are rounded to the freq_base plus the +# freq_spacing; frequency = freq_base + N * freq_spacing. This is useful at +# VHF and higher when Quisk is used with a transverter. +# This option is incompatible with "Frequency round for SSB". +freq_spacing = 0 +#freq_spacing = 25000 +#freq_spacing = 15000 + +## freq_round_ssb Frequency round for SSB, integer +# If freq_round_ssb is not zero, when the left mouse button is clicked +# the frequency is rounded for voice modes but not for CW. Mouse wheel etc. are unaffected. +# This is useful for HF when many SSB, AM etc. stations are at multiples of 500 or 1000 Hertz. +# This option is incompatible with "Frequency rounding spacing". +freq_round_ssb = 0 +#freq_round_ssb = 1000 + +## freq_base Frequency rounding base, integer +# If freq_spacing is not zero, frequencies are rounded to the freq_base plus the +# freq_spacing; frequency = freq_base + N * freq_spacing. This is useful at +# VHF and higher when Quisk is used with a transverter. +# This option is incompatible with "Frequency round for SSB". +freq_base = 0 +#freq_base = 12500 + +## invertSpectrum Invert the RF spectrum, integer choice +# If your mixing scheme inverts the RF spectrum, set this option to 1 to un-invert it. +# Otherwise set it to 0. +invertSpectrum = 0 +#invertSpectrum = 1 + +# This is a list of mixer settings. It only works for Linux; it has no effect in Windows. +# Use "amixer -c 1 contents" to get a list of mixer controls and their numid's for +# card 1 (or "-c 0" for card 0). Then make a list of (device_name, numid, value) +# for each control you need to set. For a decimal fraction, use a Python float; for example, +# use "1.0", not the integer "1". +#mixer_settings = [ +# ("hw:1", 2, 0.80), # numid of microphone volume control, volume 0.0 to 1.0; +# ("hw:1", 1, 1) # numid of capture on/off control, turn on with 1; +# ] + +## modulation_index FM modulation index, number +# For FM transmit, this is the modulation index. +modulation_index = 1.67 + +## pulse_audio_verbose_output Debug level, integer +# This controls how much debug information to print to the file quisk_logfile.txt +# and to the debug screen. +# Use 0 to turn off all debug information except for serious errors. The debug screen will not appear. +# Use 1 or 2 for more debug information. +# Changes are immediate (no need to restart). +pulse_audio_verbose_output = 0 +#pulse_audio_verbose_output = 1 +#pulse_audio_verbose_output = 2 + +## favorites_file_path Path to favorites file, text +# The quisk config screen has a "favorites" tab where you can enter the frequencies and modes of +# stations. The data is stored in this file. If this is blank, the default is the file +# quisk_favorites.txt in the directory where your config file is located. +favorites_file_path = '' + +## reverse_tx_sideband Reverse Tx sideband, integer +# Set to 1 if you want to reverse the sideband when transmitting. +# For example, to receive on LSB but transmit on USB. This may be necessary for satellite operation +# depending on the mixing scheme. +# Changes are immediate (no need to restart). +reverse_tx_sideband = 0 +#reverse_tx_sideband = 1 + +## dc_remove_bw DC remove bandwidth, integer +# This is the 3 dB bandwidth of the filter centered at zero Hertz that is used to remove DC bias. +# Choose a bandwidth that suppresses DC and low frequency noise. +# Enter 1 to select a different filter based on block removal. +# Enter zero to disable the filter. +# Changes are immediate (no need to restart). +#dc_remove_bw = 0 +#dc_remove_bw = 1 +#dc_remove_bw = 20 +#dc_remove_bw = 50 +dc_remove_bw = 100 +#dc_remove_bw = 200 +#dc_remove_bw = 400 + + + + +################ Remote +# DX cluster telent login data, thanks to DJ4CM. Must have station_display_lines > 0. +## dxClHost Dx cluster host name, text +# The Dx cluster options log into a Dx cluster server, and put station information +# on the station window under the graph and waterfall screens. +# dxClHost is the telnet host name. +dxClHost = '' +#dxClHost = 'example.host.net' + +## dxClPort Dx cluster port number, integer +# The Dx cluster options log into a Dx cluster server, and put station information +# on the station window under the graph and waterfall screens. +# dxClPort is the telnet port number. +dxClPort = 7373 + +## user_call_sign Call sign for Dx cluster, text +# The Dx cluster options log into a Dx cluster server, and put station information +# on the station window under the graph and waterfall screens. +# user_call_sign is your call sign which may be needed for login. +user_call_sign = '' + +## dxClPassword Password for Dx cluster, text +# The Dx cluster options log into a Dx cluster server, and put station information +# on the station window under the graph and waterfall screens. +# dxClPassword is the telnet password for the server. +dxClPassword = '' +#dxClPassword = 'getsomedx' + +## dxClExpireTime Dx cluster expire minutes, integer +# The Dx cluster options log into a Dx cluster server, and put station information +# on the station window under the graph and waterfall screens. +# dxClExpireTime is the time in minutes until DX Cluster entries are removed. +dxClExpireTime = 20 + +## IQ_Server_IP Pulse server IP address, text +#IP Address for remote PulseAudio IQ server. +IQ_Server_IP = "" + +## hamlib_ip IP address for Hamlib Rig 2, text +# You can control Quisk from Hamlib. Set the Hamlib rig to 2 and the device for rig 2 to +# localhost:4575. Or choose a different name and port here. Set the same name and port +# in the controlling program. +# hamlib_ip is the IP name or address. +hamlib_ip = "localhost" + +## hamlib_port IP port for Hamlib, integer +# You can control Quisk from Hamlib. For direct control, set the external program to rig 2 +# "Hamlib NET rigctl", and set the Quisk hamlib port to 4532. To use the rigctld program to control +# Quisk, set the Quisk hamlib port to 4575. To turn off Hamlib control, set the Quisk port to zero. +#hamlib_port = 4575 +hamlib_port = 4532 +#hamlib_port = 0 + +## digital_xmlrpc_url URL for control by XML-RPC, text +# This option is used by the digital modes that send audio to an external +# program, and receive audio to transmit. Set Fldigi to upper sideband, XML-RPC control. +digital_xmlrpc_url = "http://localhost:7362" +#digital_xmlrpc_url = "" + +## lin_hamlib_com1_name CAT serial port name, text +# Quisk can read HamLib commands from a serial port. Use the HamLib radio type "Flex", and enter the port name here. +# The serial port can be a real hardware port with a name starting with "/dev/". +# Or to connect to an external program like N1MM+ or WSJT-X enter a file name such as "/tmp/QuiskTTYx" +# where "x" is 0, 1, 2, etc. Quisk will create a virtual serial port when it starts. +# Enter that name into the other program. This is addition to the +# "Hamlib NET rigctl" mechanism which is based on a network connection. Leave this blank +# to turn off the serial port. The port settings are 9600 baud, 8 bits of data, no parity and one stop bit. +lin_hamlib_com1_name = "" +#lin_hamlib_com1_name = "/dev/ttyUSB0" +#lin_hamlib_com1_name = "/dev/ttyS0" +#lin_hamlib_com1_name = "/tmp/QuiskTTY0" + +## lin_hamlib_com2_name CAT serial-2 name, text +# This is a second serial port for external control of Quisk. Use a different port name. +lin_hamlib_com2_name = "" +#lin_hamlib_com2_name = "/dev/ttyUSB1" +#lin_hamlib_com2_name = "/dev/ttyS1" +#lin_hamlib_com2_name = "/tmp/QuiskTTY1" + +## win_hamlib_com1_name CAT serial port name, text +# Quisk can read HamLib commands from a serial port. Use the HamLib radio type "Flex", and enter the port name here. +# The serial port can be a real hardware port. Or to connect to an external program like N1MM+ or WSJT-X +# create a pair of virtual serial ports with a program like vspMgr or HHD Software. +# Then enter the second name into the other program. This control method is in addition to the +# "Hamlib NET rigctl" mechanism which is based on a network connection. Leave this blank +# to turn off the serial port. The port settings are 9600 baud, 8 bits of data, no parity and one stop bit. +win_hamlib_com1_name = "" +#win_hamlib_com1_name = "COM5" +#win_hamlib_com1_name = "COM6" + +## win_hamlib_com2_name CAT serial-2 name, text +# This is a second serial port for external control of Quisk. Use a different port name. +win_hamlib_com2_name = "" +#win_hamlib_com2_name = "COM15" +#win_hamlib_com2_name = "COM16" + +## remote_radio_ip Remote radio IP or name, text +# Quisk can be used as a control head to control a real radio located remotely. +# This is the IP adddress or the host name of the remote radio. +remote_radio_ip = "" +#remote_radio_ip = "192.168.1.56" + +## remote_radio_password Password for remote radio, text +# Quisk can be used as a control head to control a real radio located remotely. +# For security it requires the same password to be entered on both computers. +# Enter a fairly long pass phrase of 20 or more characters. +# It is only necessary to enter it once on each computer. +remote_radio_password = "" + + + +hamlib_com1_name = "" +hamlib_com2_name = "" + + +################ Keys +## hot_key_ptt1 PTT shortcut key 1, keycode +# Set a keyboard shortcut that will press the PTT button. +# For a regular key, use the ord() of the key. For example, ord('a') or ord('b'). For the space bar +# use ord(' '). +# If you do not want a hot key, set this to None. +# Do not choose a key that interferes with other features +# on your system such as system menus. +# Changes are immediate (no need to restart). But exit the config screen to test. +hot_key_ptt1 = None +#hot_key_ptt1 = ord(' ') +#hot_key_ptt1 = ord('z') +#hot_key_ptt1 = ord('a') +#hot_key_ptt1 = wx.WXK_F5 + +## hot_key_ptt2 PTT shortcut key 2, keycode +# If the Control or Shift key must be pressed too, set that key modifier here. +# Otherwise, set wx.ACCEL_NORMAL here. +# For example, if you want control-A, set wx.ACCEL_CTRL in "PTT Key 2", and ord('a') in "PTT Key 1". +# Changes are immediate (no need to restart). But exit the config screen to test. +hot_key_ptt2 = wx.ACCEL_NORMAL +#hot_key_ptt2 = wx.ACCEL_CTRL +#hot_key_ptt2 = wx.ACCEL_SHIFT +#hot_key_ptt2 = wx.ACCEL_CTRL | wx.ACCEL_SHIFT +#hot_key_ptt2 = wx.ACCEL_ALT + +## hot_key_ptt_toggle PTT key toggle, boolean +# Set to True if you want PTT to remain on when you release the key. A second key press will +# then release PTT. This is toggle mode. If False, you must keep pressing the key, and releasing +# it will release PTT. +# Changes are immediate (no need to restart). But exit the config screen to test. +hot_key_ptt_toggle = False +#hot_key_ptt_toggle = True + +## hot_key_ptt_if_hidden PTT key if hidden, boolean +# Set to True if you want PTT to be active when the Quisk window is not visible. +# Otherwise, the Quisk window must be active and on top. +hot_key_ptt_if_hidden = False +#hot_key_ptt_if_hidden = True + +## midi_cwkey_device Midi device name, text +# The name of the MIDI device that will be used for CW keying, PTT or other control. Leave this blank if you +# are not using MIDI. +midi_cwkey_device = '' + +## midi_cwkey_note Midi note for CW key, integer +# If you have a CW key attached to a MIDI device, enter the note number for the CW key. +# MIDI devices can send KeyUp and KeyDown messages that can be used for CW keying. +# MIDI note numbers are integers. Middle C is number 60. The A above (440 Hz) is 69. +# For microcontrollers, refer to its documentation for the correct note number. +midi_cwkey_note = -1 +#midi_cwkey_note = 60 +#midi_cwkey_note = 69 + +## midi_ptt_toggle Midi PTT toggle, boolean +# Set to True if you want PTT to remain on when you release the Midi PTT button. A second button press will +# then release PTT. This is toggle mode. If False, you must keep pressing the button, and releasing +# it will release PTT. +# Changes are immediate (no need to restart). +midi_ptt_toggle = False +#midi_ptt_toggle = True + + + + +################ Windows +# Station info display configuration, thanks to DJ4CM. This displays a window of station names +# below the graph frequency (X axis). + +## station_display_lines Number of station lines, integer +# The number of station info display lines below the graph X axis. +station_display_lines = 1 +#station_display_lines = 0 +#station_display_lines = 3 + +## display_fraction Display fraction, number +# This is the fraction of spectrum to display from zero to one. It causes the edges +# of the display to be suppressed. For example, 0.85 displays the central 85% of the spectrum. +display_fraction = 1.00 + +## default_screen Startup screen, text choice +# Select the default screen when Quisk starts. +default_screen = 'Graph' +#default_screen = 'WFall' +#default_screen = 'Config' + +## graph_width Startup graph width, number +# The width of the graph data as a fraction of the total screen size. This +# controls the width of the Quisk window, but +# will be adjusted by Quisk to accommodate preferred FFT sizes. +# It can not be made too small because +# of the space needed for all the buttons. +graph_width = 0.8 + +## window_width Window width pixels, integer +# The use of startup graph width provides an optimal size for PC screens. But when running +# full screen, for example, on a tablet screen or a dedicated display, greater control +# is required. These options exactly set the Quisk window geometry. When window pixel width +# is used, graph width is ignored. You may need to reduce button_font_size. Use -1 +# to ignore this feature, and use graph width. +window_width = -1 +#window_width = 640 + +## window_height Window height pixels, integer +# The use of startup graph width provides an optimal size for PC screens. But when running +# full screen, for example, on a tablet screen or a dedicated display, greater control +# is required. These options exactly set the Quisk window geometry. When window pixel width +# is used, graph width is ignored. You may need to reduce button_font_size. Use -1 +# to ignore this feature, and use graph width. +window_height = -1 +#window_height = 480 + +## window_posX Window X position, integer +# The use of startup graph width provides an optimal size for PC screens. But when running +# full screen, for example, on a tablet screen or a dedicated display, greater control +# is required. These options exactly set the Quisk window geometry. When window pixel width +# is used, graph width is ignored. You may need to reduce button_font_size. Use -1 +# to ignore this feature, and use graph width. +window_posX = -1 +#window_posX = 0 + +## window_posY Window Y position, integer +# The use of startup graph width provides an optimal size for PC screens. But when running +# full screen, for example, on a tablet screen or a dedicated display, greater control +# is required. These options exactly set the Quisk window geometry. When window pixel width +# is used, graph width is ignored. You may need to reduce button_font_size. Use -1 +# to ignore this feature, and use graph width. +window_posY = -1 +#window_posY = 0 + +## button_layout Button layout, text choice +# This option controls how many buttons are displayed on the screen. The large screen +# layout is meant for a PC. The small screen layout is meant for small touch screens, and +# small screens used in embedded systems. +button_layout = 'Large screen' +#button_layout = 'Small screen' + + +# These are the initial values for the Y-scale and Y-zero sliders for each screen. +# The sliders go from zero to 160. +graph_y_scale = 100 +graph_y_zero = 0 +waterfall_y_scale = 80 # Initial value; new values are saved for each band +waterfall_y_zero = 40 # Initial value; new values are saved for each band +waterfall_graph_y_scale = 100 +waterfall_graph_y_zero = 60 +scope_y_scale = 80 +scope_y_zero = 0 # Currently doesn't do anything +filter_y_scale = 90 +filter_y_zero = 0 + +# Select the way the waterfall screen scrolls: +# waterfall_scroll_mode = 0 # scroll at a constant rate. +waterfall_scroll_mode = 1 # scroll faster at the top so that a new signal appears sooner. + +# Select the initial size in pixels (minimum 1) of the graph at the top of the waterfall. +waterfall_graph_size = 80 + +# Quisk saves radio settings in a settings file. The default directory is the same as the config +# file, and the file name is quisk_settings.json. You can set a different name here. If you dual +# boot Windows and Linux, you can set the same path in your Windows and Linux config files, so that +# settings are shared. Even if Windows and Linux settings are shared, the sound device names and a +# few other settings are kept separate. +settings_file_path = '' +#settings_file_path = /path/to/my/file/quisk_settings.json + + + + +################ Timing and CW + +## lin_latency_millisecs Play latency msec, integer +# Play latency determines how many samples are in the radio sound play buffer. +# A larger number makes it less likely that you will run out of samples to play, +# but increases latency. It is OK to suffer a certain number of play buffer +# underruns in order to get lower latency. +lin_latency_millisecs = 150 +#lin_latency_millisecs = 50 +#lin_latency_millisecs = 100 +#lin_latency_millisecs = 250 + +## win_latency_millisecs Play latency msec, integer +# Play latency determines how many samples are in the radio sound play buffer. +# A larger number makes it less likely that you will run out of samples to play, +# but increases latency. It is OK to suffer a certain number of play buffer +# underruns in order to get lower latency. +win_latency_millisecs = 150 +#win_latency_millisecs = 50 +#win_latency_millisecs = 100 +#win_latency_millisecs = 250 + +latency_millisecs = 150 + + +## lin_data_poll_usec Hardware poll usecs, integer +# Quisk polls the hardware for samples at intervals. This is the poll time in microseconds. +# A lower time reduces latency. A higher time is less taxing on the hardware. +#lin_data_poll_usec = 5000 +#lin_data_poll_usec = 10000 +#lin_data_poll_usec = 15000 +#lin_data_poll_usec = 20000 + +## win_data_poll_usec Hardware poll usecs, integer +# Quisk polls the hardware for samples at intervals. This is the poll time in microseconds. +# A lower time reduces latency. A higher time is less taxing on the hardware. +#win_data_poll_usec = 15000 +#win_data_poll_usec = 5000 +#win_data_poll_usec = 10000 +#win_data_poll_usec = 20000 + +if sys.platform == "win32": + data_poll_usec = 5000 # poll time in microseconds +else: + data_poll_usec = 5000 # poll time in microseconds + +## fft_size_multiplier FFT size multiplier, integer +# The fft_size is the width of the data on the screen (about 800 to +# 1200 pixels) times the fft_size_multiplier. Multiple FFTs are averaged +# together to achieve your graph refresh rate. If fft_size_multiplier is +# too small you will get many fft errors. You can specify fft_size_multiplier, +# or enter a large number (use 9999) to maximize it, or enter zero to let +# quisk calculate it for you. +# Your fft_size_multiplier should have many small factors. Avoid 7 and 13, and +# use 8 or 12 instead. +# If your hardware can change the decimation, there are further compilcations. +# The FFT size is fixed, and only the average count can change to adjust the +# refresh rate. +fft_size_multiplier = 0 + +## graph_refresh Graph refresh Hertz, integer +# The graph_refresh is the frequency at which the graph is updated, +# and should be about 5 to 10 Hertz. Higher rates require more processor power. +graph_refresh = 7 + +## start_cw_delay Start CW delay msec, integer +# Quisk generates its own CW waveform when keyed by the serial port or MIDI. Quisk delays this CW waveform +# so that when changing from Rx to Tx there is time for relays to switch and power amps to turn on. +# The CW key timing is preserved. This is the delay in milliseconds from the first +# CW key press until RF output. The delay can be zero to 250 milliseconds. +# If the key is attached to the hardware and the hardware generates the CW, Quisk can not implement this delay. +# Changes are immediate (no need to restart). +start_cw_delay = 15 + +## start_ssb_delay Start SSB delay msec, integer +# This discards the first few milliseconds of RF output when starting a transmission +# so that relays can switch and power amps can turn on. +# It operates in all modes except CW. +# It should be long enough to allow filters to fill with samples. +# Changes are immediate (no need to restart). +start_ssb_delay = 100 + +## maximum_tx_secs Maximum Tx seconds, integer +# This changes Quisk from transmit to receive after the specified number of seconds. +# It is meant to be a failsafe timer in case Quisk is controlled remotely by another program and the link fails. +# Enter zero to turn off the failsafe timer. If Quisk receives another Tx command, the timer restarts. +# Changes are immediate (no need to restart). +maximum_tx_secs = 0 + +## keyupDelay Keyup delay msecs, integer +# This is the key hang time for semi-breakin CW. It is the time in milliseconds +# from the last CW key release until changing to receive. +# It only operates in CW mode. +# Changes are immediate (no need to restart). +keyupDelay = 500 + +## cwTone CW tone frequency in Hertz, integer +# This is the CW tone frequency in Hertz. +# Changes are immediate (no need to restart). +cwTone = 600 +#cwTone = 400 +#cwTone = 800 + +## use_sidetone Use sidetone, integer choice +# This controls whether Quisk will display a sidetone volume control +# and generate a CW sidetone. +use_sidetone = 0 +#use_sidetone = 1 + +## use_fast_sound Use fast sound, boolean +# This turns on the new faster sound system which provides a much faster and more useful sidetone +# for CW operation. For Linux you must use an Alsa device for the radio sound playback device. +# There are no special requirements for Windows. +use_fast_sound = False +#use_fast_sound = True + +## lin_quisk_serial_port Quisk serial port, text +# This is the serial port used for a CW key and a PTT connection. +# Changes are immediate (no need to restart). +lin_quisk_serial_port = "" +#lin_quisk_serial_port = "/dev/ttyUSB0" +#lin_quisk_serial_port = "/dev/ttyUSB1" + +## win_quisk_serial_port Quisk serial port, text +# This is the serial port used for a CW key and a PTT connection. +# Changes are immediate (no need to restart). +win_quisk_serial_port = "" +#win_quisk_serial_port = "COM6" +#win_quisk_serial_port = "COM7" + +quisk_serial_port = "" + +## quisk_serial_cts Use CTS for, text choice +# You can use the CTS signal of the serial port for a CW key or for push-to-talk PTT. +# If "when low", the CW key or PTT is asserted (transmitting) if the voltage on the pin is low, +# and similarly for "when high". +# Changes are immediate (no need to restart). +quisk_serial_cts = "None" +#quisk_serial_cts = "CW when high" +#quisk_serial_cts = "CW when low" +#quisk_serial_cts = "PTT when high" +#quisk_serial_cts = "PTT when low" + +## quisk_serial_dsr Use DSR for, text choice +# You can use the DSR signal of the serial port for a CW key or for push-to-talk PTT. +# If "when low", the CW key or PTT is asserted (transmitting) if the voltage on the pin is low, +# and similarly for "when high". +# Changes are immediate (no need to restart). +quisk_serial_dsr = "None" +#quisk_serial_dsr = "CW when high" +#quisk_serial_dsr = "CW when low" +#quisk_serial_dsr = "PTT when high" +#quisk_serial_dsr = "PTT when low" + + + +################ Controls + +## graph_peak_hold_1 Graph peak hold 1, number +# This controls the speed of the graph peak hold for the two settings +# of the Graph button. Lower numbers give a longer time constant. +graph_peak_hold_1 = 0.25 + +## graph_peak_hold_2 Graph peak hold 2, number +# This controls the speed of the graph peak hold for the two settings +# of the Graph button. Lower numbers give a longer time constant. +graph_peak_hold_2 = 0.10 + + +## add_imd_button Add IMD button, integer choice +# If you want Quisk to add a button to generate a 2-tone IMD test signal, +# set this to 1. +add_imd_button = 0 +#add_imd_button = 1 + +## add_extern_demod Add ext demod button, text +# If you want to write your own I/Q filter and demodulation module, set +# this to the name of the button to add, and change extdemod.c. +add_extern_demod = "" +#add_extern_demod = "WFM" + +## add_fdx_button Add FDX button, integer choice +# If you want Quisk to add a full duplex button (transmit and receive at the +# same time), set this to 1. +add_fdx_button = 0 +#add_fdx_button = 1 + +## add_freedv_button Add FreeDv button, integer choice +# These buttons add up to two additional mode buttons after CW, USB, etc. +# Set this to add the FDV mode button for digital voice: +add_freedv_button = 1 +#add_freedv_button = 0 + +## freedv_tx_msg FreeDv Tx message, text +# For freedv, this is the text message to send. +freedv_tx_msg = '' +#freedv_tx_msg = 'N2XXX Jim, New Jersey, USA \n' + + +# This is the list of FreeDV modes and their index number. The starting mode is the first listed. +freedv_modes = (('Mode 1600', 0), + ('Mode 2400A', 3), ('Mode 2400B', 4), ('Mode 800XA', 5), + ('Mode 700C', 6), ('Mode 700D', 7), ('Mode 2020', 8), ('Mode 700E', 13)) + +# These are the filter bandwidths for each mode. Quisk has built-in optimized filters +# for these values, but you can change them if you want. +FilterBwCW = (200, 400, 600, 1000, 1500, 3000) +FilterBwSSB = (2000, 2200, 2500, 2800, 3000, 3300) +FilterBwAM = (4000, 5000, 6000, 8000, 10000, 9000) +FilterBwFM = (8000, 10000, 12000, 16000, 18000, 20000) +FilterBwIMD = FilterBwSSB +FilterBwDGT = (200, 400, 1500, 3200, 4800, 10000) +FilterBwEXT = (8000, 10000, 12000, 15000, 17000, 20000) +FilterBwFDV = (1500, 2000, 3000, '', '', '') + +# If your hardware file defines the method OnButtonPTT(self, event), then Quisk will +# display a PTT button you can press. The method must switch your hardware to +# transmit somehow, for example, by setting a serial port pin to high. + +## spot_button_keys_tx Key Tx on Spot, boolean +# If you want the Spot button to key the transmitter immediately when you press it, set this option. +spot_button_keys_tx = True +#spot_button_keys_tx = False + + + +# Thanks to Christof, DJ4CM, for button fonts. +################ Fonts + +## button_font_size Button font size, integer +# If the Quisk screen is too wide or the buttons are too crowded, perhaps due to a low screen +# resolution, you can reduce the font sizes. +button_font_size = 10 +#button_font_size = 9 +#button_font_size = 8 + +## default_font_size Default font size, integer +# These control the font size on the named screen. +default_font_size = 12 + +## status_font_size Status font size, integer +# These control the font size on the named screen. +status_font_size = 14 + +## config_font_size Config font size, integer +# These control the font size on the named screen. +config_font_size = 14 + +## graph_font_size Graph font size, integer +# These control the font size on the named screen. +graph_font_size = 10 + +## graph_msg_font_size Graph message font size, integer +# These control the font size on the named screen. +graph_msg_font_size = 14 + +## favorites_font_size Favorites font size, integer +# These control the font size on the named screen. +favorites_font_size = 14 + +## lin_quisk_typeface Typeface, text +# This controls the typeface used in fonts. The objective is to choose an available font that +# offers good support for the Unicode characters used on buttons and windows. +#lin_quisk_typeface = '' + +## win_quisk_typeface Typeface, text +# This controls the typeface used in fonts. The objective is to choose an available font that +# offers good support for the Unicode characters used on buttons and windows. +#win_quisk_typeface = 'Lucida Sans Unicode' +#win_quisk_typeface = 'Arial Unicode MS' + +if sys.platform == "win32": + quisk_typeface = 'Lucida Sans Unicode' + #quisk_typeface = 'Arial Unicode MS' +else: + quisk_typeface = '' + +## use_unicode_symbols Use Unicode symbols, boolean +# This controls whether the "U" unicode symbols or the "T" text symbols are used on buttons and windows. +# You can change the "U" and "T" symbols to anything you want in your config file. +use_unicode_symbols = True +#use_unicode_symbols = False + +# These are the Unicode symbols used in the station window. Thanks to Christof, DJ4CM. +Usym_stat_fav = u"\u2605" # Symbol for favorites, a star +Usym_stat_mem = u"\u24C2" # Symbol for memory stations, an "M" in a circle +#Usym_stat_dx = u"\u2691" # Symbol for DX Cluster stations, a flag +Usym_stat_dx = u"\u25B2" # Symbol for DX Cluster stations, a Delta +# These are the text symbols used in the station window. +Tsym_stat_fav = 'F' +Tsym_stat_mem = 'M' +Tsym_stat_dx = 'Dx' +# +# These are the Unicode symbols to display on buttons. Thanks to Christof, DJ4CM. +Ubtn_text_range_dn = u"\u2B07" # Down band, left arrow +Ubtn_text_range_up = u"\u2B06" # Up band, right arrow +Ubtn_text_play = u"\u25BA" # Play button +Ubtn_text_rec = u"\u25CF" # Record button, a filled dot +Ubtn_text_file_rec = "File " + u"\u25CF" # Record to file +Ubtn_text_file_play = "File " + u"\u25BA" # Play from file +Ubtn_text_fav_add = u"\u2605" + u"\u2191" # Add to favorites +Ubtn_text_fav_recall = u"\u2605" + u"\u2193" # Jump to favorites screen +Ubtn_text_mem_add = u"\u24C2" + u"\u2191" # Add to memory +Ubtn_text_mem_next = u"\u24C2" + u"\u27B2" # Next memory +Ubtn_text_mem_del = u"\u24C2" + u"\u2613" # Delete from memory +# These are the text symbols to display on buttons. +Tbtn_text_range_dn = "Dn" +Tbtn_text_range_up = "Up" +Tbtn_text_play = "Tmp Play" +Tbtn_text_rec = "Tmp Rec" +Tbtn_text_file_rec = "File Rec" +Tbtn_text_file_play = "File Play" +Tbtn_text_fav_add = ">Fav" +Tbtn_text_fav_recall = "Fav" +Tbtn_text_mem_add = "Save" +Tbtn_text_mem_next = "Next" +Tbtn_text_mem_del = "Del" + +## decorate_buttons Decorate buttons, boolean +# This controls whether to add the button decorations that mark cycle and adjust buttons. +decorate_buttons = True +#decorate_buttons = False + +btn_text_cycle = u"\u21B7" # Character to display on multi-push buttons +btn_text_cycle_small = u"\u2193" # Smaller version when there is little space +btn_text_switch = u"\u21C4" # Character to switch left-right + +## color_scheme Color scheme, text choice +# This controls the color scheme used by Quisk. The default color scheme is A, and you can change this scheme +# in your config file. Other color schemes are available here. +color_scheme = 'A' +#color_scheme = 'B' +#color_scheme = 'C' + +## waterfall_palette Waterfall colors, text choice +# This controls the colors used in the waterfall. The default color scheme is A, and you can change this scheme +# in your config file. Other color schemes are available here. +waterfall_palette = 'A' +#waterfall_palette = 'B' +#waterfall_palette = 'C' + + + + +################ Colors +# Thanks to Steve Murphy, KB8RWQ for the patch adding additional color control. +# Thanks to Christof, DJ4CM for the patch adding additional color control. +# Define colors used by all widgets in wxPython colour format. +# This is the default color scheme, color scheme A. You can change these colors in your config file: +color_bg = 'light steel blue' # Lower screen background +color_bg_txt = 'black' # Lower screen text color +color_graph = 'lemonchiffon1' # Graph background +color_config2 = 'lemonchiffon3' # color in tab row of config screen +color_gl = 'grey' # Lines on the graph +color_graphticks = 'black' # Graph ticks +color_graphline = '#005500' # graph data line color +color_graphlabels = '#555555' # graph label color +color_btn = 'steelblue2' # button color +color_check_btn = 'yellow2' # color of a check button when it is checked +color_cycle_btn = 'goldenrod3' # color of a cycle button when it is checked +color_adjust_btn = 'orange3' # color of an adjustable button when it is checked +color_test = 'hot pink' # color of a button used for test (turn off for tx) +color_freq = 'lightcyan1' # background color of frequency and s-meter +color_freq_txt = 'black' # text color of frequency display +color_entry = color_freq # frequency entry box +color_entry_txt = 'black' # text color of entry box +color_enable = 'black' # text color for an enabled button +color_disable = 'white' # text color for a disabled button +color_popchoice = 'maroon' # text color for button that pops up a row of buttons +color_bandwidth = 'lemonchiffon3' # color for bandwidth display; thanks to WB4JFI +color_txline = 'red' # vertical line color for tx in graph +color_rxline = 'green' # vertical line color for rx in graph +color_graph_msg_fg = 'black' # text messages on the graph screen +color_graph_msg_bg = 'lemonchiffon2' # background of text messages on the graph screen + +# This color scheme B, a dark color scheme designed by Steve Murphy, KB8RWQ. +# Additional colors added by N2ADR. +color_scheme_B = { +'color_bg' : '#111111', +'color_bg_txt' : 'white', +'color_graph' : '#111111', +'color_config2' : '#111111', +'color_gl' : '#555555', +'color_graphticks' : '#DDDDDD', +'color_graphline' : '#00AA00', +'color_graphlabels' : '#FFFFFF', +'color_btn' : '#666666', +'color_check_btn' : '#996699', +'color_cycle_btn' : '#666699', +'color_adjust_btn' : '#669999', +'color_test' : 'hot pink', +'color_freq' : '#333333', +'color_freq_txt' : 'white', +'color_entry' : '#333333', +'color_entry_txt' : 'white', +'color_enable' : 'white', +'color_disable' : 'black', +'color_popchoice' : 'maroon', +'color_bandwidth' : '#333333', +'color_txline' : 'red', +'color_rxline' : 'green', +'color_graph_msg_fg' : 'white', +'color_graph_msg_bg' : '#111111', +} + +# This is color scheme C: +####################################################################################### +# +# Color scheme designed by Sergio, IK8HTM. 04/06/2016 +# '#red red green green blue blue' x00 to xFF +# '#FFFFFF' = white +# '#000000' = black +# +####################################################################################### +color_scheme_C = { +'color_bg' : '#123456', +'color_bg_txt' : '#FFFFFF', +'color_graph' : 'lightcyan3', +'color_config2' : '#0000FF', +'color_gl' : '#555555', +'color_graphticks' : '#DDDDDD', +'color_graphline' : '#00AA00', +'color_graphlabels' : '#000000', +'color_btn' : '#223344', +'color_check_btn' : '#A07315', +'color_cycle_btn' : '#0031C4', +'color_adjust_btn' : '#669999', +'color_test' : '#E73EE7', +'color_freq' : '#333333', +'color_freq_txt' : '#FEF80A', +'color_entry' : '#333333', +'color_entry_txt' : '#FEF80A', +'color_enable' : '#FFFFFF', +'color_disable' : '#000000', +'color_popchoice' : '#D76B00', +'color_bandwidth' : 'lemonchiffon1', +'color_txline' : '#FF0000', +'color_rxline' : '#3CC918', +'color_graph_msg_fg' : '#000000', +'color_graph_msg_bg' : 'lemonchiffon2', +} +############################################################################################# + + +# These are the palettes for the waterfall. The one used is named waterfallPallette, +# so to use a different one, overwrite this name in your configuration file. +waterfallPalette = ( + ( 0, 0, 0, 0), + ( 36, 85, 0, 255), + ( 73, 153, 0, 255), + (109, 255, 0, 128), + (146, 255, 119, 0), + (182, 85, 255, 100), + (219, 255, 255, 0), + (255, 255, 255, 255) + ) +digipanWaterfallPalette = ( + ( 0, 0, 0, 0), + ( 32, 0, 0, 62), + ( 64, 0, 0, 126), + ( 96, 145, 142, 96), + (128, 181, 184, 48), + (160, 223, 226, 105), + (192, 254, 254, 4), + (255, 255, 58, 0) + ) + +waterfallPaletteB = ( # from David Fainitski +(0, 0, 0, 0), +(13, 0, 14, 14), +(26, 0, 40, 40), +(39, 0, 73, 73), +(43, 0, 94, 94), +(56, 0, 115, 121), +(69, 0, 87, 190), +(72, 0, 110, 252), +(85, 0, 166, 252), +(98, 0, 216, 252), +(112, 0, 247, 234), +(125, 2, 255, 124), +(138, 5, 255, 64), +(151, 154, 255, 0), +(164, 219, 255, 0), +(177, 247, 250, 0), +(190, 254, 233, 0), +(214, 254, 185, 0), +(227, 255, 125, 0), +(241, 255, 59, 0), +(255, 255, 0, 0) +) + +waterfallPaletteC = ( # from David Fainitski +(0, 0, 0, 0), +(32, 0, 25, 25), +(64, 6, 58, 41), +(96, 16, 78, 43), +(128, 29, 120, 41), +(160, 51, 144, 35), +(192, 116, 141, 43), +(224, 195, 198, 35), +(255, 245, 99, 3) +) + +# This is the data used to draw colored lines on the frequency X axis to +# indicate CW and Phone sub-bands. You can make it anything you want. +# These are the colors used for sub-bands: +CW = '#FF4444' # General class CW +eCW = '#FF8888' # Extra class CW +Phone = '#4444FF' # General class phone +ePhone = '#8888FF' # Extra class phone +# ARRL band plan special frequencies +Data = '#FF9900' +DxData = '#CC6600' +RTTY = '#DA5529' +SSTV = '#FFFF00' +AM = '#00FF00' +Packet = '#00FFFF' +Beacons = '#6FAECA' +Satellite = '#22AA88' +Repeater = '#AA00FF' # Repeater outputs +RepInput = '#AA88FF' # Repeater inputs +Simplex = '#00BEFF' +Special = '#FF69B4' +Other = '#888888' +RxOnly = '#AAAAAA' +# Colors start at the indicated frequency and continue until the +# next frequency. The special color "None" turns off color. +# + + + + +################ Bands +# Band plans vary by country, so they can be changed here. +# To change BandPlan in your config file, first remove any frequencies in the range +# you want to change; then add your frequencies; and then sort the list. Or you could just +# replace the whole list. +# These are the suppressed carrier frequencies for 60 meters +freq60 = (5330500, 5346500, 5357000, 5371500, 5403500) +# Band plan +BandPlan = [ + # Test display of colors + #[ 0, CW], [ 50000, eCW], [ 100000, Phone], [ 150000, ePhone], [ 200000, Data], [ 250000, DxData], [ 300000, RTTY], [ 350000, SSTV], + #[ 400000, AM], [ 450000, Packet], [ 500000, Beacons], [ 550000, Satellite], [ 600000, Repeater], [ 650000, RepInput], [ 700000, Simplex], + #[ 750000, Other], [ 800000, Special], [ 850000, None], + # 137k + [ 135700, Data], + [ 137800, None], + # 500k + [ 472000, Data], + [ 479000, None], + # 160 meters + [ 1800000, Data], + [ 1809000, Other], + [ 1811000, CW], + [ 1843000, Phone], + [ 1908000, Other], + [ 1912000, Phone], + [ 1995000, Other], + [ 2000000, None], + # 80 meters + [ 3500000, eCW], + [ 3525000, CW], + [ 3570000, Data], + [ 3589000, DxData], + [ 3591000, Data], + [ 3600000, ePhone], + [ 3790000, Other], + [ 3800000, Phone], + [ 3844000, SSTV], + [ 3846000, Phone], + [ 3880000, AM], + [ 3890000, Phone], + [ 4000000, None], + # 60 meters + [ freq60[0], Phone], + [ freq60[0] + 2800, None], + [ freq60[1], Phone], + [ freq60[1] + 2800, None], + [ freq60[2], Phone], + [ freq60[2] + 2800, None], + [ freq60[3], Phone], + [ freq60[3] + 2800, None], + [ freq60[4], Phone], + [ freq60[4] + 2800, None], + # 40 meters + [ 7000000, eCW], + [ 7025000, CW], + [ 7039000, DxData], + [ 7041000, CW], + [ 7080000, Data], + [ 7125000, ePhone], + [ 7170000, SSTV], + [ 7172000, ePhone], + [ 7175000, Phone], + [ 7285000, AM], + [ 7295000, Phone], + [ 7300000, None], + # 30 meters + [10100000, CW], + [10130000, RTTY], + [10140000, Packet], + [10150000, None], + # 20 meters + [14000000, eCW], + [14025000, CW], + [14070000, RTTY], + [14095000, Packet], + [14099500, Other], + [14100500, Packet], + [14112000, CW], + [14150000, ePhone], + [14225000, Phone], + [14229000, SSTV], + [14231000, Phone], + [14281000, AM], + [14291000, Phone], + [14350000, None], + # 17 meters + [18068000, CW], + [18100000, RTTY], + [18105000, Packet], + [18110000, Phone], + [18168000, None], + # 15 meters + [21000000, eCW], + [21025000, CW], + [21070000, RTTY], + [21110000, CW], + [21200000, ePhone], + [21275000, Phone], + [21339000, SSTV], + [21341000, Phone], + [21450000, None], + # 12 meters + [24890000, CW], + [24920000, RTTY], + [24925000, Packet], + [24930000, Phone], + [24990000, None], + # 10 meters + [28000000, CW], + [28070000, RTTY], + [28150000, CW], + [28200000, Beacons], + [28300000, Phone], + [28679000, SSTV], + [28681000, Phone], + [29000000, AM], + [29200000, Phone], + [29300000, Satellite], + [29520000, Repeater], + [29590000, Simplex], + [29610000, Repeater], + [29700000, None], + # 6 meters + [50000000, Beacons], + [50100000, Phone], + [54000000, None], + # 4 meters + [70000000, Phone], + [70500000, None], + # 2 meters + [144000000, CW], + [144200000, Phone], + [144275000, Beacons], + [144300000, Satellite], + [144380000, Special], + [144400000, Satellite], + [144500000, RepInput], + [144900000, Other], + [145100000, Repeater], + [145500000, Other], + [145800000, Satellite], + [146010000, RepInput], + [146400000, Simplex], + [146510000, Special], # Simplex calling frequency + [146530000, Simplex], + [146610000, Repeater], + [147420000, Simplex], + [147600000, RepInput], + [148000000, None], + # 1.25 meters + [222000000, Phone], + [222250000, RepInput], + [223400000, Simplex], + [223520000, Data], + [223640000, Repeater], + [225000000, None], + #70 centimeters + [420000000, SSTV], + [432000000, Satellite], + [432070000, Phone], + [432300000, Beacons], + [432400000, Phone], + [433000000, Repeater], + [435000000, Satellite], + [438000000, Repeater], + [445900000, Simplex], + [445990000, Special], # Simplex calling frequency + [446010000, Simplex], + [446100000, Repeater], + [450000000, None], + # 33 centimeters + [902000000, Other], + [928000000, None], + # 23 centimeters + [1240000000, Other], + [1300000000, None], + # 13 centimeters + [2300000000, Other], + [2450000000, None], + # 9 centimeters + [3300000000, Other], + [3500000000, None], + # 5 centimeters + [5650000000, Other], + [5925000000, None], + # 3 centimeters + [10000000000, Other], + [10500000000, None], + ] + +# List of all bands in order of frequency: +BandList = [ '137k', '500k', '160', '80', '60', '40', '30', '20', '17', '15', '12', '10', '6', '4', '2', + '1.25', '70cm' , '33cm', '23cm', '13cm', '9cm', '5cm', '3cm'] + +## BandEdge Band Edge, dict +# For each band, this dictionary gives the lower and upper band edges. Frequencies +# outside these limits will not be remembered as the last frequency in the band. +BandEdge = { + '137k':( 135700, 137800), '500k':( 472000, 479000), + '160':( 1800000, 2000000), '80' :( 3500000, 4000000), + '60' :( 5300000, 5430000), '40' :( 7000000, 7300000), + '30' :(10100000, 10150000), '20' :(14000000, 14350000), + '17' :(18068000, 18168000), '15' :(21000000, 21450000), + '12' :(24890000, 24990000), '10' :(28000000, 29700000), + '6' :( 50000000, 54000000), + '4' :( 70000000, 70500000), + '2' :( 144000000, 148000000), + '1.25' :( 222000000, 225000000), + '70cm' :( 420000000, 450000000), + '33cm' :( 902000000, 928000000), + '23cm' :(1240000000, 1300000000), + '13cm' :(2300000000, 2450000000), + '9cm' :(3300000000, 3500000000), + '5cm' :(5650000000, 5925000000), + '3cm' :(10000000000,10500000000), + 'Aux1' :(0,0), + } + +# For the Time band, this is the center frequency, tuning frequency and mode: +bandTime = [ + ( 2500000-10000, 10000, 'AM'), + ( 3330000-10000, 10000, 'AM'), + ( 5000000-10000, 10000, 'AM'), + ( 7850000-10000, 10000, 'AM'), + (10000000-10000, 10000, 'AM'), + (14670000-10000, 10000, 'AM'), + (15000000-10000, 10000, 'AM'), + (20000000-10000, 10000, 'AM'), + ] + +## bandLabels Band Buttons, list +# This is the list of band buttons that Quisk displays, and it should have +# a length of 14 or less. Empty buttons can have a null string "" label. +# Note that the 60 meter band and the Time band have buttons that support +# multiple presses. +bandLabels = [ + 'Audio', '160', '80', ('60',) * 5, '40', '30', '20', '17', + '15', '12', '10', ('Time',) * len(bandTime)] + +# This is a dictionary of shortcut keys for each band. If you do not want a shortcut, use ''. The shortcut +# character will be underlined in the label if present. +bandShortcuts = {'Audio':'', 'Aux1':'1', '160':'1', '80':'8', '60':'6', '40':'4', '30':'3', '20':'2', '17':'7', + '15':'5', '12':'1', '10':'0', 'Time':'e', '6':'6', '4':'4', '2':'2', '1.25':'5', '70cm':'7', + '33cm':'3', '23cm':'', '13cm':'', '9cm':'', '5cm':'', '3cm':''} + +## bandTransverterOffset Transverter Offset, dict +# If you use a transverter, you need to tune your hardware to a frequency lower than +# the frequency displayed by Quisk. For example, if you have a 2 meter transverter, +# you may need to tune your hardware from 28 to 30 MHz to receive 144 to 146 MHz. +# Enter the transverter offset in Hertz in this dictionary. For this to work, your +# hardware must support it. Currently, the HiQSDR, SDR-IQ and SoftRock are supported. bandTransverterOffset = {'2': 144000000 - 28000000} +bandTransverterOffset = {} + + + + +################ Obsolete +filter_display = 1 # Display the filter bandwidth on the graph screen; 0 or 1; thanks to WB4JFI +# For each band, this dictionary gives the initial center frequency, tuning +# frequency as an offset from the center frequency, and the mode. This is +# no longer too useful because the persistent_state feature saves and then +# overwrites these values anyway. +bandState = {'Audio':(0, 0, 'LSB'), + '160':( 1890000, -10000, 'LSB'), '80' :( 3660000, -10000, 'LSB'), + '60' :( 5370000, 1500, 'USB'), '40' :( 7180000, -5000, 'LSB'), '30':(10120000, -10000, 'CWL'), + 'Time':( 5000000, 0, 'AM')} +for band in BandEdge: + f1, f2 = BandEdge[band] + if f1 > 13500000: + f = (f1 + f2) // 2 + f = (f + 5000) // 10000 + f *= 10000 + bandState[band] = (f, 10000, 'USB') +# Select the method to test the state of the key; see is_key_down.c +key_method = "" # No keying, or internal method +# key_method = "/dev/parport0" # Use the named parallel port +# key_method = "/dev/ttyS0" # Use the named serial port +# key_method = "192.168.1.44" # Use UDP from this address +# +# Quisk can save its current state in a file on exit, and restore it when you restart. +# State includes band, frequency and mode, but not every item of state (not screen). +# The file is .quisk_init.pkl in the same directory as your config file. If this file +# becomes corrupted, just delete it and it will be reconstructed. +#persistent_state = False +persistent_state = True +# Select the default mode when Quisk starts (overruled by persistent_state): +# default_mode = 'FM' +default_mode = 'USB' +# If you use a soundcard with Ethernet control of the VFO, set these parameters: +rx_ip = "" # Receiver IP address for VFO control +# This determines what happens when you tune by dragging the mouse. The correct +# choice depends on how your hardware performs tuning. You may want to use a +# custom hardware file with a custom ChangeFrequency() method too. +mouse_tune_method = 0 # The Quisk tune frequency changes and the VFO frequency is unchanged. +#mouse_tune_method = 1 # The Quisk tune frequency is unchanged and the VFO changes. +# configurable mouse wheel thanks to DG7MGY +mouse_wheelmod = 50 # Round frequency when using mouse wheel (50 Hz) diff --git a/quisk_conf_kx3.py b/quisk_conf_kx3.py new file mode 100644 index 0000000..eb7b6b8 --- /dev/null +++ b/quisk_conf_kx3.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import +# Please do not change this configuration file for Quisk. Copy it to +# your own config file and make changes there. +# +# This config file is for hamlib control of a KX3 through hamlib, with Quisk +# acting as a panadapter. The daemon rigctld must be running. The open() method +# below tries to start it, or you can start it by hand. + +import sys, os + +if sys.platform == "win32": + name_of_sound_capt = 'Primary' + name_of_sound_play = 'Primary' + latency_millisecs = 150 + data_poll_usec = 20000 +else: + name_of_sound_capt = 'hw:0' + name_of_sound_play = 'hw:0' + latency_millisecs = 150 + data_poll_usec = 5000 + +# Use the hamlib hardware module to talk to the KX3 +from quisk_hardware_hamlib import Hardware as BaseHardware + +class Hardware(BaseHardware): + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + # Change the port and timing parameters here: + # self.hamlib_rigctld_port = 4532 # Standard rigctld control port + # self.hamlib_poll_seconds = 0.2 # Time interval to poll for changes + def open(self): + ret = BaseHardware.open(self) + if not self.hamlib_connected: # rigctld is not started. Try to start it. + os.system("rigctld -m 229 -r /dev/ttyUSB0 -s 4800 & ") # Check the baud rate menu setting + # If this fails, start rigctld by hand. + return ret + diff --git a/quisk_conf_model.py b/quisk_conf_model.py new file mode 100644 index 0000000..f847f06 --- /dev/null +++ b/quisk_conf_model.py @@ -0,0 +1,28 @@ +# This is a sample quisk_conf.py configuration file for a soundcard. + +# Please do not change this sample file. +# Instead copy it to your own .quisk_conf.py and make changes there. +# See quisk_conf_defaults.py for more information. + +# The default hardware module was already imported. Import a different one here. +# import quisk_hardware_fixed as quisk_hardware + +# In ALSA, soundcards have these names. The "hw" devices are the raw +# hardware devices, and should be used for soundcard capture. +#name_of_sound_capt = "hw:0" +#name_of_sound_capt = "hw:1" +#name_of_sound_capt = "plughw" +#name_of_sound_capt = "plughw:1" +#name_of_sound_capt = "default" + +# Pulseaudio support added by Philip G. Lee. Many thanks! +# For PulseAudio support, use the name "pulse" and connect the streams +# to your hardware devices using a program like pavucontrol +#name_of_sound_capt = "pulse" + +sample_rate = 96000 # ADC hardware sample rate in Hertz +name_of_sound_capt = "hw:0" # Name of soundcard capture hardware device. +name_of_sound_play = name_of_sound_capt # Use the same device for play back +channel_i = 0 # Soundcard index of in-phase channel: 0, 1, 2, ... +channel_q = 1 # Soundcard index of quadrature channel: 0, 1, 2, ... + diff --git a/quisk_conf_openradio.py b/quisk_conf_openradio.py new file mode 100644 index 0000000..18f6f63 --- /dev/null +++ b/quisk_conf_openradio.py @@ -0,0 +1,122 @@ +# OpenRadio v1.1 Quisk Configuration File +# +# IMPORTANT: To be able to control the OpenRadio board from within Quisk, +# you will need to compile and upload the 'openradio_quisk' firmware, which +# is available from: https://github.com/darksidelemm/open_radio_miniconf_2015 +# +# You will also need to install the pyserial package for python. +# + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + + +# SOUND CARD SETTINGS +# +# Uncomment these if you wish to use PortAudio directly +#name_of_sound_capt = "portaudio:(hw:2,0)" +#name_of_sound_play = "portaudio:(hw:1,0)" + +# Uncomment these lines if you wish to use Pulseaudio +name_of_sound_capt = "pulse" +name_of_sound_play = "pulse" + +# SERIAL PORT SETTINGS +# Set this as appropriate for your OS. +openradio_serial_port = "/dev/ttyUSB0" +openradio_serial_rate = 57600 + + +# OpenRadio Frequency limits. +# These are just within the limits set in the openradio_quisk firmware. +openradio_lower = 100001 +openradio_upper = 29999999 + +# OpenRadio Hardware Control Class +# +import serial,time +from quisk_hardware_model import Hardware as BaseHardware + +class Hardware(BaseHardware): + def open(self): + # Called once to open the Hardware + # Open the serial port. + self.or_serial = serial.Serial(openradio_serial_port,openradio_serial_rate,timeout=3) + print("Opened Serial Port.") + # Wait for the Arduino Nano to restart and boot. + time.sleep(2) + # Poll for version. Should probably confirm the response on this. + version = str(self.get_parameter("VER")) + print(version) + # Return an informative message for the config screen + t = version + ". Capture from sound card %s." % self.conf.name_of_sound_capt + return t + + def close(self): + # Called once to close the Hardware + self.or_serial.close() + + def ChangeFrequency(self, tune, vfo, source='', band='', event=None): + # Called whenever quisk requests a frequency change. + # This sends the FREQ command to set the centre frequency of the OpenRadio, + # and will also move the 'tune' frequency (the section within the RX passband + # which is to be demodulated) if it falls outside the passband (+/- sample_rate/2). + print("Setting VFO to %d." % vfo) + if(vfoopenradio_upper): + vfo = openradio_upper + print("Outside range! Setting to %d" % openradio_upper) + + success = self.set_parameter("FREQ",str(vfo)) + + # If the tune frequency is outside the RX bandwidth, set it to somewhere within that bandwidth. + if(tune>(vfo + sample_rate/2) or tune<(vfo - sample_rate/2)): + tune = vfo + 10000 + print("Bringing tune frequency back into the RX bandwidth.") + + if success: + print("Frequency change succeeded!") + else: + print("Frequency change failed.") + + return tune, vfo + +# +# Serial comms functions, to communicate with the OpenRadio board +# + + def get_parameter(self,string): + self.or_serial.write(string+"\n") + return self.get_argument() + + def set_parameter(self,string,arg): + self.or_serial.write(string+","+arg+"\n") + if self.get_argument() == arg: + return True + else: + return False + + def get_argument(self): + data1 = self.or_serial.readline() + # Do a couple of quick checks to see if there is useful data here + if len(data1) == 0: + return -1 + + # Maybe we didn't catch an OK line? + if data1.startswith('OK'): + data1 = self.or_serial.readline() + + # Check to see if we have a comma in the string. If not, there is no argument. + if data1.find(',') == -1: + return -1 + + data1 = data1.split(',')[1].rstrip('\r\n') + + # Check for the OK string + data2 = self.or_serial.readline() + if data2.startswith('OK'): + return data1 diff --git a/quisk_conf_peaberry.py b/quisk_conf_peaberry.py new file mode 100644 index 0000000..bb6630b --- /dev/null +++ b/quisk_conf_peaberry.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import +from softrock import hardware_usb_new as quisk_hardware +from softrock import widgets_tx as quisk_widgets + +si570_direct_control = True +si570_xtal_freq = 114211833 + +sample_rate = 48000 +playback_rate = 48000 +name_of_sound_capt = "hw:1,0" +name_of_sound_play = "plughw:0,0" +channel_i = 0 +channel_q = 1 + +usb_vendor_id = 0x16c0 +usb_product_id = 0x05dc diff --git a/quisk_conf_sdr8600.py b/quisk_conf_sdr8600.py new file mode 100644 index 0000000..9f03eb4 --- /dev/null +++ b/quisk_conf_sdr8600.py @@ -0,0 +1,30 @@ +from __future__ import absolute_import +from __future__ import division +# These are the configuration parameters for receiving the +# 10.7 MHz IF output of the AOR AR8600 receiver with the +# SDR-IQ by RfSpace. This results in a 100 kHz to 3 GHz +# wide range receiver with pan adapter. + +# Please do not change this sample file. +# Instead copy it to your own .quisk_conf.py and make changes there. +# See quisk_conf_defaults.py for more information. + +# Use this hardware module to control the AR8600 and SDR-IQ +import quisk_hardware_sdr8600 as quisk_hardware + +# Start in FM mode +default_mode = 'FM' + +use_sdriq = 1 # Use the SDR-IQ +#sdriq_name = "/dev/ft2450" # Name of the SDR-IQ device to open +sdriq_name = "/dev/ttyUSB2" # Name of the SDR-IQ device to open +sdriq_clock = 66666667.0 # actual sample rate (66666667 nominal) +sdriq_decimation = 1250 # Must be 360, 500, 600, or 1250 +sample_rate = int(float(sdriq_clock) / sdriq_decimation + 0.5) # Don't change this +name_of_sound_capt = "" # We do not capture from the soundcard +name_of_sound_play = "hw:0" # Play back on this soundcard +# Note: For the SDR-IQ, playback is stereo at 48000 Hertz. +channel_i = 0 # Soundcard index of left channel +channel_q = 1 # Soundcard index of right channel +display_fraction = 0.85 # The edges of the full bandwidth are not valid + diff --git a/quisk_conf_sdriq.py b/quisk_conf_sdriq.py new file mode 100644 index 0000000..9b249fc --- /dev/null +++ b/quisk_conf_sdriq.py @@ -0,0 +1,34 @@ +from __future__ import absolute_import +from __future__ import division +# These are the configuration parameters for Quisk using the +# SDR-IQ by RfSpace as the capture device. + +# Please do not change this sample file. +# Instead copy it to your own .quisk_conf.py and make changes there. +# See quisk_conf_defaults.py for more information. + +# In ALSA, soundcards have these names: +#name_of_sound_play = "hw:0" +#name_of_sound_play = "hw:1" +#name_of_sound_play = "plughw" +#name_of_sound_play = "plughw:1" +#name_of_sound_play = "default" + +# Pulseaudio support added by Philip G. Lee. Many thanks! +# For PulseAudio support, use the name "pulse" and connect the streams +# to your hardware devices using a program like pavucontrol +#name_of_sound_capt = "pulse" + +use_sdriq = 1 # Use the SDR-IQ +#sdriq_name = "/dev/ft2450" # Name of the SDR-IQ device to open +sdriq_name = "/dev/ttyUSB2" # Name of the SDR-IQ device to open +sdriq_clock = 66666667.0 # actual sample rate (66666667 nominal) +sdriq_decimation = 1250 # Must be 360, 500, 600, or 1250 +sample_rate = int(float(sdriq_clock) / sdriq_decimation + 0.5) # Don't change this +name_of_sound_capt = "" # We do not capture from the soundcard +name_of_sound_play = "hw:0" # Play back on this soundcard +playback_rate = 48000 # Radio sound play rate +channel_i = 0 # Soundcard index of left channel +channel_q = 1 # Soundcard index of right channel +display_fraction = 0.85 # The edges of the full bandwidth are not valid + diff --git a/quisk_conf_win.py b/quisk_conf_win.py new file mode 100644 index 0000000..ae3b59e --- /dev/null +++ b/quisk_conf_win.py @@ -0,0 +1,50 @@ +# This is a sample quisk_conf.py configuration file for Microsoft Windows. + +# For Windows, your default config file name is "My Documents/quisk_conf.py", +# but you can use a different config file by using -c or --config. Quisk creates +# an initial default config file if there is none. To control Quisk, edit +# "My Documents/quisk_conf.py" using any text editor; for example WordPad (not Notepad). + +# In Windows you can see what sound devices you have, and you can set the Primary +# Device for capture and playback by using Control Panel/Sounds and Audio Devices. +# If you have only one sound device, it should be set as "Primary". If you have +# several, find the names by using Control Panel/Sounds and Audio Devices; for +# example, you may have "SoundMAX HD Audio" in the list for "Sound playback" and +# "Sound recording". To specify this device for capture (recording) or playback, +# enter a unique part of its name using exact upper/lower case. For example: +# name_of_sound_capture = "SoundMAX" +# name_of_sound_play = "SoundMAX" + +# There are many possible options for your config file. Copy the ones you want +# from the master file quisk_conf_defaults.py (but don't change the master file). +# The master config file is located in the site-packages/quisk folder for Python 2.7. + +# This file is Python code and the comment character is "#". To ignore a line, +# start it with "#". To un-ignore a line, remove the "#". Generally you must start +# lines in column one (the left edge) except for logic blocks. + + +sample_rate = 48000 # ADC hardware sample rate in Hertz +name_of_sound_capt = "Primary" # Name of soundcard capture hardware device. +name_of_sound_play = "Primary" # Use the same device for play back. +latency_millisecs = 150 # latency time in milliseconds + +# Select the default screen when Quisk starts: +default_screen = 'Graph' +#default_screen = 'WFall' + +# If you use hardware with a fixed VFO (crystal controlled SoftRock) un-comment the following: +# import quisk_hardware_fixed as quisk_hardware +# fixed_vfo_freq = 7056000 + +# If you use an SDR-IQ for capture, first install the SpectraView software +# that came with the SDR-IQ. This will install the USB driver. Then set these parameters: +# import quisk_hardware_sdriq as quisk_hardware # Use different hardware file +# use_sdriq = 1 # Capture device is the SDR-IQ +# sdriq_name = "SDR-IQ" # Name of the SDR-IQ device to open +# sdriq_clock = 66666667.0 # actual sample rate (66666667 nominal) +# sdriq_decimation = 500 # Must be 360, 500, 600, or 1250 +# sample_rate = int(float(sdriq_clock) / sdriq_decimation + 0.5) # Don't change this +# name_of_sound_capt = "" # We do not capture from the soundcard +# playback_rate = 48000 # Radio sound play rate, default 48000 +# display_fraction = 0.85 # The edges of the full bandwidth are not valid diff --git a/quisk_hardware_fifisdr.py b/quisk_hardware_fifisdr.py new file mode 100644 index 0000000..1ce131e --- /dev/null +++ b/quisk_hardware_fifisdr.py @@ -0,0 +1,156 @@ +# This is the hardware file for the FiFiSDR radio. Thanks to Joe, LA6GRA. + +#import sys, struct, time, traceback, math +import struct, traceback +from softrock import hardware_usb as SoftRock + +GET_FIFI_EXTRA = 0xAB +SET_FIFI_EXTRA = 0xAC + +EXTRA_READ_SVN_VERSION = 0 +EXTRA_READ_FW_VERSION = 1 +EXTRA_WRITE_PREAMP = 19 +EXTRA_READ_PREAMP = 19 + +try: + import usb + import usb.core, usb.util +except: + if sys.platform == 'win32': + dlg = wx.MessageDialog(None, "The Python pyusb module is required but not installed. Do you want me to install it?", + "Install Python pyusb", style = wx.YES|wx.NO) + if dlg.ShowModal() == wx.ID_YES: + import subprocess + subprocess.call([sys.executable, "-m", "pip", "install", "pyusb"]) + try: + import usb + import usb.core, usb.util + except: + dlg = wx.MessageDialog(None, "Installation of Python pyusb failed. Please install it by hand.", + "Installation failed", style=wx.OK) + dlg.ShowModal() + else: + dlg = wx.MessageDialog(None, "The Python pyusb module is required but not installed. Please install package python-usb.", + "Install Python pyusb", style = wx.OK) + dlg.ShowModal() + +DEBUG = 1 + +IN = usb.util.build_request_type(usb.util.CTRL_IN, usb.util.CTRL_TYPE_VENDOR, usb.util.CTRL_RECIPIENT_DEVICE) +OUT = usb.util.build_request_type(usb.util.CTRL_OUT, usb.util.CTRL_TYPE_VENDOR, usb.util.CTRL_RECIPIENT_DEVICE) + +UBYTE2 = struct.Struct(' 50: + sound = sound[0:30] + '|||' + sound[-17:] + if DEBUG: + print("Sound = %s" % (sound)) + + try: + #res = self.handle.controlMsg(requestType = DEVICE2HOST, + #request = GET_FIFI_EXTRA, + #buffer = 4, + #value=0, + #index=EXTRA_READ_SVN_VERSION, + #timeout=100) + res = self.usb_dev.ctrl_transfer(IN, GET_FIFI_EXTRA, 0, EXTRA_READ_SVN_VERSION, 4) + except: + if DEBUG: + traceback.print_exc() + + + svn = (((((res[3]<<8) + res[2])<<8) + res[1])<<8) + res[0] + if DEBUG: + print ("FiFi_SVN = %d" % svn) + + try: + res = self.usb_dev.ctrl_transfer(IN, GET_FIFI_EXTRA, 0, EXTRA_READ_FW_VERSION, 20) + except: + if DEBUG: + traceback.print_exc() + fifi_ver = res + if DEBUG: + print ("FiFi_ver = %s" % res) + #fifi_ver_str = ''.join([chr(i) for i in res]) # fÃ¥r ikkje med null terminering + fifi_ver_str = '' + for i in fifi_ver: + if not i: + break + fifi_ver_str += chr(i) + print ("FiFi_ver = %s" % fifi_ver_str) + + #text = 'Capture from SoftRock USB on %s, Firmware %s' % (sound, ver) + text = "Capture from FiFi-SDR USB (%d, %s), on %s, (SR ver. %s)" % (svn, fifi_ver_str, sound, ver) + if DEBUG: + print ('Quisk_title_line = "%s"' % text) + + #self.application.bottom_widgets.info_text.SetLabel(text) + if DEBUG and usb_dev: + print ('Startup freq', self.GetStartupFreq()) + print ('Run freq', self.GetFreq()) + print ('Address 0x%X' % usb_dev.ctrl_transfer(IN, 0x41, 0, 0, 1)[0]) + sm = usb_dev.ctrl_transfer(IN, 0x3B, 0, 0, 2) + sm = UBYTE2.unpack(sm)[0] + print ('Smooth tune', sm) + return text + + def OnButtonRfGain(self, event): + btn = event.GetEventObject() + value = btn.index + # value == 0: -6dB + # value == 1: 0dB + msg = bytearray() + msg.append(value) + self.usb_dev.ctrl_transfer(OUT, SET_FIFI_EXTRA, self.si570_i2c_address + 0x700, EXTRA_WRITE_PREAMP, msg) diff --git a/quisk_hardware_fixed.py b/quisk_hardware_fixed.py new file mode 100644 index 0000000..99215ae --- /dev/null +++ b/quisk_hardware_fixed.py @@ -0,0 +1,24 @@ +from __future__ import absolute_import +# Please do not change this hardware control module for Quisk. +# This hardware module is for receivers with a fixed VFO, such as +# the SoftRock. Change your VFO frequency below. + +# If you want to use this hardware module, specify it in quisk_conf.py. +# import quisk_hardware_fixed as quisk_hardware +# See quisk_hardware_model.py for documentation. + +from quisk_hardware_model import Hardware as BaseHardware + +class Hardware(BaseHardware): + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.vfo = self.conf.fixed_vfo_freq # Fixed VFO frequency in Hertz + self.tune = self.vfo + 10000 # Current tuning frequency in Hertz + def ChangeFrequency(self, tune, vfo, source='', band='', event=None): + # Change and return the tuning and VFO frequency. See quisk_hardware_model.py. + self.tune = tune + return tune, self.vfo + def ReturnFrequency(self): + # Return the current tuning and VFO frequency. See quisk_hardware_model.py. + return self.tune, self.vfo + diff --git a/quisk_hardware_hamlib.py b/quisk_hardware_hamlib.py new file mode 100644 index 0000000..7d23747 --- /dev/null +++ b/quisk_hardware_hamlib.py @@ -0,0 +1,157 @@ +# This is a hardware file for control of a rig using the Hamlib rigctld daemon. +# This hardware will not start the daemon rigctld, so you must start it yourself. +# This hardware will connect to rigctld, and rigctld connects to the rig. You can test +# rigctld with the command rigctl. See the hamlib documentation for rigctld and rigctl. +# +# If you change the frequency in Quisk, the change is sent to rigctld. This hardware +# will query (poll) rigctld for its frequency at intervals to see if the rig changed +# the frequency. If it did, the change is sent to Quisk. +# +# These are the attributes we watch: Rx frequency, mode + +from __future__ import print_function +from __future__ import absolute_import + +DEBUG = 0 + +import socket, time, traceback +import _quisk as QS + +from quisk_hardware_model import Hardware as BaseHardware + +class Hardware(BaseHardware): + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.hamlib_rigctld_port = 4532 # Standard rigctld control port + self.hamlib_poll_seconds = 0.2 # Time interval to poll for changes + self.hamlib_connected = False + self.radio_freq = None + self.radio_mode = None + self.quisk_freq = None + self.quisk_vfo = None + self.quisk_mode = 'USB' + self.received = '' + self.toggle = False + self.time0 = 0 + def open(self): + ret = BaseHardware.open(self) + self.hamlib_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.hamlib_socket.settimeout(0.0) + self.ConnectRigctld() + return ret + def close(self): + self.hamlib_socket.close() + self.hamlib_connected = False + return BaseHardware.close(self) + def ConnectRigctld(self): + if self.hamlib_connected: + return True + try: + self.hamlib_socket.connect(('localhost', self.hamlib_rigctld_port)) + except: + return False # Failure to connect + self.hamlib_connected = True + if DEBUG: print("rigctld connected") + return True # Success + def ChangeFrequency(self, tune, vfo, source='', band='', event=None): + self.quisk_freq = tune + self.quisk_vfo = tune + if DEBUG: print('Change', source, tune) + return self.quisk_freq, self.quisk_vfo + def ReturnFrequency(self): + # Return the current tuning and VFO frequency. If neither have changed, + # you can return (None, None). This is called at about 10 Hz by the main. + return self.quisk_freq, self.quisk_vfo + def ChangeMode(self, mode): # Change the tx/rx mode + # mode is a string: "USB", "AM", etc. + if mode == 'CWU': + mode = 'CW' + elif mode == 'CWL': + mode = 'CW' + elif mode[0:4] == 'DGT-': + mode = 'USB' + self.quisk_mode = mode + if DEBUG: print('Change', mode) + def ChangeBand(self, band): + # band is a string: "60", "40", "WWV", etc. + pass + def HeartBeat(self): # Called at about 10 Hz by the main + if not self.hamlib_connected: # Continually try to connect + try: + self.hamlib_socket.connect(('localhost', self.hamlib_rigctld_port)) + except: + return + else: + self.hamlib_connected = True + if DEBUG: print("rigctld Connected") + self.ReadHamlib() + if time.time() - self.time0 < self.hamlib_poll_seconds: + return + self.time0 = time.time() + if self.quisk_mode != self.radio_mode: + self.HamlibSend("|M %s 0\n" % self.quisk_mode) + elif self.quisk_freq != self.radio_freq: + self.HamlibSend("|F %d\n" % self.quisk_freq) + elif self.toggle: + self.toggle = False + self.HamlibSend("|f\n") # Poll for frequency + else: + self.toggle = True + self.HamlibSend("|m\n") # Poll for mode + def HamlibSend(self, text): + if DEBUG: print('Send', text, end=' ') + try: # Patch thanks to Rolandas, LY0NAS + self.hamlib_socket.sendall(text.encode('utf-8', errors='ignore')) + except socket.error: + pass + def ReadHamlib(self): + if not self.hamlib_connected: + return + try: # Read any data from the socket + text = self.hamlib_socket.recv(1024).decode('utf-8', errors='replace') + except socket.timeout: # This does not work + pass + except socket.error: # Nothing to read + pass + else: # We got some characters + self.received += text + while '\n' in self.received: # A complete response ending with newline is available + reply, self.received = self.received.split('\n', 1) # Split off the reply, save any further characters + reply = reply.strip() # Here is our reply + if reply[-6:] != 'RPRT 0': + if DEBUG: print('Reject', reply) + continue + try: + if reply[0:9] == 'set_freq:': # set_freq: 18120472|RPRT 0 + freq, status = reply[9:].split('|') + freq = int(freq) + if DEBUG: print(' Radio S freq', freq) + self.radio_freq = freq + elif reply[0:9] == 'get_freq:': # get_freq:|Frequency: 18120450|RPRT 0 + z, freq, status = reply.split('|') + z, freq = freq.split(':') + freq = int(freq) + if DEBUG: print(' Radio G freq', freq) + if self.quisk_freq == self.radio_freq: + self.radio_freq = freq + self.ChangeFrequency(freq, self.quisk_vfo, 'hamlib') + elif reply[0:9] == 'set_mode:': # set_mode: FM 0|RPRT 0 + mode, status = reply[9:].split('|') + mode, z = mode.split() + if DEBUG: print(' Radio S mode', mode) + self.radio_mode = mode + elif reply[0:9] == 'get_mode:': # get_mode:|Mode: FM|Passband: 12000|RPRT 0 + z, mode, passb, status = reply.split('|') + z, mode = mode.split() + if DEBUG: print(' Radio G mode', mode) + if self.quisk_mode == self.radio_mode: + if self.radio_mode != mode: # The radio changed the mode + self.radio_mode = mode + self.quisk_mode = mode + if mode in ('CW', 'CWR'): + mode = 'CWU' + self.application.modeButns.SetLabel(mode, True) # Set mode + else: + if DEBUG: print('Unknown', reply) + except: + if DEBUG: traceback.print_exc() diff --git a/quisk_hardware_hl2_oob.py b/quisk_hardware_hl2_oob.py new file mode 100644 index 0000000..eae9092 --- /dev/null +++ b/quisk_hardware_hl2_oob.py @@ -0,0 +1,63 @@ +# This hardware file is for use with the Hermes Lite 2. It disables the power amplifier when the +# transmit frequency including the sidebands is outside of the band selected. Enter this file name +# quisk_hardware_hl2_oob.py as your hardware file on the Config/radio/Hardware screen. + +from __future__ import print_function +from __future__ import absolute_import + +from hermes.quisk_hardware import Hardware as BaseHardware + +class Hardware(BaseHardware): + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.bandEdge1 = 0 + self.bandEdge2 = 0 + def ChangeMode(self, mode): + BaseHardware.ChangeMode(self, mode) + self.FixBandEdge() + def ChangeBand(self, band): + BaseHardware.ChangeBand(self, band) + self.FixBandEdge() + def FixBandEdge(self): # Reduce the band edges accordig to the transmit mode sidebands + if self.band in ("Audio", "Time"): # Rx only + freq1 = 0 + freq2 = 0 + else: + try: + freq1, freq2 = self.conf.BandEdge[self.band] + except: + freq1 = 0 + freq2 = 0 + mode = self.mode + if mode in ("CWL", "CWU"): + freq1 += 40 + freq2 -= 40 + elif mode in ("USB", "DGT-U", "FDV-U", "IMD"): + freq2 -= 3000 + elif mode in ("LSB", "DGT-L", "FDV-L"): + freq1 += 3000 + elif mode == "AM": + freq1 += 3000 + freq2 -= 3000 + elif mode in ("FM", "DGT-FM"): + freq1 += 8000 + freq2 -= 8000 + else: + freq1 += 3000 + freq2 -= 3000 + self.bandEdge1 = freq1 + self.bandEdge2 = freq2 + def HeartBeat(self): + BaseHardware.HeartBeat(self) + power_amp_enabled = self.pc2hermes[37] & 0b1000 + if self.bandEdge1 <= self.tx_frequency <= self.bandEdge2: # Tx frequency is in band + if not power_amp_enabled and self.conf.hermes_power_amp: + #print ("Turn HL2 power amp on") + self.SetControlBit(0x09, 19, 1) + elif power_amp_enabled and not self.conf.hermes_power_amp: # Should not happen + #print ("Turn HL2 power amp ??") + self.SetControlBit(0x09, 19, 0) + else: # Tx frequency is out of band + if power_amp_enabled: + #print ("Turn HL2 power amp off") + self.SetControlBit(0x09, 19, 0) diff --git a/quisk_hardware_model.py b/quisk_hardware_model.py new file mode 100644 index 0000000..37f5664 --- /dev/null +++ b/quisk_hardware_model.py @@ -0,0 +1,157 @@ +# Please do not change this hardware control module for Quisk. +# You should use it as a base class for your own hardware modules. + +# A custom hardware module should subclass this module; start it with: +# from quisk_hardware_model import Hardware as BaseHardware +# class Hardware(BaseHardware): +# def __init__(self, app, conf): +# BaseHardware.__init__(self, app, conf) +# ### your module starts here + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import _quisk as QS + +class Hardware: + def __init__(self, app, conf): + self.application = app # Application instance (to provide attributes) + self.conf = conf # Config file module + self.rf_gain_labels = () # Do not add the Rf Gain button + self.correct_smeter = conf.correct_smeter # Default correction for S-meter + self.use_sidetone = conf.use_sidetone # Copy from the config file + self.transverter_offset = 0 # Calculate the transverter offset in Hertz for each band + self.hermes_ip = '' # Should not be necessary + def pre_open(self): # Quisk calls this once before open() is called + pass + def open(self): # Quisk calls this once to open the Hardware + # Return an informative message for the config screen. + # This method must return a string showing whether the open succeeded or failed. + t = "Capture from sound card %s." % self.conf.name_of_sound_capt + return t + def post_open(self): # Quisk calls this once after open() and after sound is started + pass + def close(self): # Quisk calls this once to close the Hardware + pass + def ChangeFrequency(self, tune, vfo, source='', band='', event=None): + # Change and return the tuning and VFO frequency in Hertz. The VFO frequency is the + # frequency in the center of the display; that is, the RF frequency corresponding to an + # audio frequency of zero Hertz. The tuning frequency is the RF frequency indicated by + # the tuning line on the display, and is equivalent to the transmit frequency. The quisk + # receive frequency is the tuning frequency plus the RIT (receive incremental tuning). + # If your hardware will not change to the requested frequencies, return different + # frequencies. + # The source is a string indicating the source of the change: + # BtnBand A band button + # BtnUpDown The band Up/Down buttons + # FreqEntry The user entered a frequency in the box + # MouseBtn1 Left mouse button press + # MouseBtn3 Right mouse button press + # MouseMotion The user is dragging with the left button + # MouseWheel The mouse wheel up/down + # NewDecim The decimation changed + # For "BtnBand", the string band is in the band argument. + # For the mouse events, the handler event is in the event argument. + return tune, vfo + def ReturnFrequency(self): + # Return the current tuning and VFO frequency. If neither have changed, + # you can return (None, None). This is called at about 10 Hz by the main. + # return (tune, vfo) # return changed frequencies + return None, None # frequencies have not changed + def ReturnVfoFloat(self): + # Return the accurate VFO frequency as a floating point number. + # You can return None to indicate that the integer VFO frequency is valid. + return None + def ChangeMode(self, mode): # Change the tx/rx mode + # mode is a string: "USB", "AM", etc. + pass + def ChangeBand(self, band): + # band is a string: "60", "40", "WWV", etc. + try: + self.transverter_offset = self.conf.bandTransverterOffset[band] + except: + self.transverter_offset = 0 + def OnButtonPTT(self, event): + pass + def OnBtnFDX(self, is_fdx): # Status of FDX button, 0 or 1 + pass + def HeartBeat(self): # Called at about 10 Hz by the GUI thread + pass + def FastHeartBeat(self): # Called frequently by the GUI thread + pass + # The "VarDecim" methods are used to change the hardware decimation rate. + # If VarDecimGetChoices() returns any False value, no other methods are called. + def VarDecimGetChoices(self): # Return a list/tuple of strings for the decimation control. + return False # Return a False value for no decimation changes possible. + def VarDecimGetLabel(self): # Return a text label for the decimation control. + return '' + def VarDecimGetIndex(self): # Return the index 0, 1, ... of the current decimation. + return 0 # This is called before open() to initialize the control. + def VarDecimSet(self, index=None): # Called when the control is operated. + # Change the decimation here, and return the sample rate. The index is 0, 1, 2, .... + # Called with index == None before open() to set the initial sample rate. + # Note: The last used sample rate is available as self.application.vardecim_set if + # the persistent state option is True. If the value is unavailable for + # any reason, self.application.vardecim_set is None. + return 48000 + def VarDecimRange(self): # Return the lowest and highest sample rate. + return (48000, 960000) + # + # The following methods are used to return I/Q samples from the hardware file to Quisk. + # None of these methods are called unless you call InitSamples(). + # For an example of their use, see quisk_hardware_sdriq.py. + # Quisk calls all methods in the hardware file from the GUI thread except for StartSamples(), + # GetRxSamples() and StopSamples() which are called from the sound thread. + # The sound thread starts with StartSamples() and is not running during the calls to pre_open() and open(). + def InitSamples(self, int_size, endian): # Rx sample initialization; you must call this from your hardware __init__(). + # int_size is the number of bytes in each I or Q sample: 1, 2, 3, or 4 + # endian is the order of bytes in the sample: 0 == little endian; 1 == big endian + # This can be called again to change the format. For example, a different number of bytes for different sample rates. + QS.set_params(rx_bytes=int_size, rx_endian=endian) + self.application.samples_from_python = True + def InitBscope(self, int_size, endian, clock, length): # Bandscope initialization; accept raw samples from the ADC + # You may call this once from your hardware __init__() after calling InitSamples(). The bandscope format can not be changed. + # int_size is the number of bytes in each sample: 1, 2, 3, or 4 + # endian is the order of bytes in the sample: 0 == little endian; 1 == big endian + # clock is the integer ADC sample rate in Hertz + # length is the number of samples in each block of ADC samples, and equals the FFT size. + QS.set_params(bscope_bytes=int_size, bscope_endian=endian, bscope_size=length) + self.application.bandscope_clock = clock + #def PollCwKey(self): # Optional. Called frequently by the sound thread to check the CW key status. + # pass # Do not define if not needed. + def StartSamples(self): # Quisk calls this from the sound thread to start sending samples. + # If you return a string, it replaces the string returned from hardware open() + pass + def StopSamples(self): # Quisk calls this from the sound thread to stop sending samples. + pass + def GetRxSamples(self): # Quisk calls this frequently from the sound thread. Poll your hardware for samples. + # Return any available samples by calling AddRxSamples() and perhaps AddBscopeSamples() from within this method. + pass + def AddRxSamples(self, samples): # Call this from within GetRxSamples() to record the Rx samples. + # "samples" is int_size of integer I data followed by int_size of integer Q data, repeated. + # For Python 3, "samples" must be a byte array or bytes; use s = bytearray(2), or s = b"\x55\x44" or similar. + # For Python 2, "samples" must be a byte array or bytes or a string. + # The byte length must represent a whole number of samples. No partial records. + QS.add_rx_samples(samples) + def AddBscopeSamples(self, samples): # Call this from within GetRxSamples() to record the bandscope samples. + # "samples" is the whole block of integer samples from the ADC. + # For Python 3, "samples" must be a byte array or bytes; use s = bytearray(2), or s = b"\x55\x44" or similar. + # For Python 2, "samples" must be a byte array or bytes or a string. + # The number of bytes in "samples" must equal the block length times the bytes per sample. + QS.add_bscope_samples(samples) + def GotClip(self): # Call this to indicate that samples were received with the clip (overrange) indicator true. + QS.set_params(clip=1) + def GotReadError(self, print_msg, msg): # Call this to indicate an error in receiving the Rx samples. + if print_msg: + print(msg) + QS.set_params(read_error=1) + # If you import wx, there a few useful functions available. To set a busy cursor do this: + # try: + # wx.BeginBusyCursor() + # wx.Yield() + # self.ReallyTimeConsumingOperation() + # finally: + # wx.EndBusyCursor() + # To update the GUI during a long running operation, you can use wx.Yield() or wx.SafeYield(). + diff --git a/quisk_hardware_sdr8600.py b/quisk_hardware_sdr8600.py new file mode 100644 index 0000000..a0f65e0 --- /dev/null +++ b/quisk_hardware_sdr8600.py @@ -0,0 +1,71 @@ +from __future__ import absolute_import +# Please do not change this hardware control module for Quisk. Instead copy +# it to your own quisk_hardware.py and make changes there. +# See quisk_hardware_model.py for documentation. +# +# This hardware module sends the IF output of an AOR AR8600 +# to the input of an SDR-IQ by RfSpace +# +# Note: The AR8600 IF output in WFM mode seems to tune in 10kHz increments +# no matter what the step size, even though the display reads a +# different frequency. + +import time +import _quisk as QS +import serial # From the pyserial package + +# Use the SDR-IQ hardware as the base class +from quisk_hardware_sdriq import Hardware as BaseHardware + +class Hardware(BaseHardware): + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.vfo_frequency = 0 # current vfo frequency + self.tty_name = '/dev/ttyUSB0' # serial port name for AR8600 + self.serial = None # the open serial port + self.timer = 0.02 # time between AR8600 commands in seconds + self.time0 = 0 # time of last AR8600 command + self.serial_out = [] # send commands slowly + def open(self): + self.serial = serial.Serial(port=self.tty_name, baudrate=9600, + stopbits=serial.STOPBITS_TWO, xonxoff=1, timeout=0) + self.SendAR8600('MD0\r') # set WFM mode so the IF output is available + # The AR8600 inverts the spectrum of the 2 meter and 70 cm bands. + # Other bands may not be inverted, so we may need to test the frequency. + # But this is not currently implemented. + QS.invert_spectrum(1) + t = BaseHardware.open(self) # save the message + BaseHardware.ChangeFrequency(10700000, 10700000) + return t + def close(self): + BaseHardware.close(self) + if self.serial: + self.serial.write('EX\r') + time.sleep(1) # wait for output to drain, but don't block + self.serial.close() + self.serial = None + def ChangeFrequency(self, rx_freq, vfo_freq, source='', band='', event=None): + vfo_freq = (vfo_freq + 5000) / 10000 * 10000 # round frequency + if vfo_freq != self.vfo_frequency and vfo_freq >= 100000: + self.vfo_frequency = vfo_freq + self.SendAR8600('RF%010d\r' % vfo_freq) + return rx_freq, vfo_freq + def ChangeBand(self, band): # Defeat base class method + return + def SendAR8600(self, msg): # Send commands to the AR8600, but not too fast + if self.serial: + if time.time() - self.time0 > self.timer: + self.serial.write(msg) # send message now + self.time0 = time.time() + else: + self.serial_out.append(msg) # send message later + def HeartBeat(self): # Called at about 10 Hz by the main + BaseHardware.HeartBeat(self) + if self.serial: + chars = self.serial.read(1024) + #if chars: + # print chars + if self.serial_out and time.time() - self.time0 > self.timer: + self.serial.write(self.serial_out[0]) + self.time0 = time.time() + del self.serial_out[0] diff --git a/quisk_hardware_sdriq.py b/quisk_hardware_sdriq.py new file mode 100644 index 0000000..4fc93df --- /dev/null +++ b/quisk_hardware_sdriq.py @@ -0,0 +1,491 @@ +# Please do not change this hardware control module. +# It provides support for the SDR-IQ by RfSpace. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import wx, traceback, types +try: + import serial +except: + serial = None +from quisk_hardware_model import Hardware as BaseHardware + +DEBUG = 0 + +if sys.version_info.major > 2: + Q3StringTypes = str +else: + Q3StringTypes = (str, unicode) + +class Hardware(BaseHardware): + decimations = [1250, 600, 500, 360] + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.rf_gain_labels = ('RF +30', 'RF +20', 'RF +10', 'RF 0 dB') + if conf.fft_size_multiplier == 0: + conf.fft_size_multiplier = 3 # Set size needed by VarDecim + self.clock = int(conf.sdriq_clock) + self.device = Sdriq(self, conf.sdriq_name, self.clock, conf.sdriq_decimation) # SDR-IQ hardware access + self.busy_cursor = False # Is the busy cursor displayed? + rx_bytes = 2 # rx_bytes is the number of bytes in each I or Q sample: 1, 2, 3, or 4 + rx_endian = 0 # rx_endian is the order of bytes in the sample array: 0 == little endian; 1 == big endian + self.InitSamples(rx_bytes, rx_endian) # Initialize: read samples from this hardware file and send them to Quisk + def open(self): # This method must return a string showing whether the open succeeded or failed. + return self.device.open() + def close(self): + self.device.close() + def OnButtonRfGain(self, event): + """Set the SDR-IQ preamp gain and attenuator state. + + self.device.SetGain(gstate, gain) + gstate == 0: Gain must be 0, -10, -20, or -30 + gstate == 1: Attenuator is on and gain is 0 to 127 (7 bits) + gstate == 2: Attenuator is off and gain is 0 to 127 (7 bits) + gain for 34, 24, 14, 4 db is 127, 39, 12, 4. + """ + btn = event.GetEventObject() + n = btn.index + if n == 0: + self.device.SetGain(2, 127) + elif n == 1: + self.device.SetGain(2, 39) + elif n == 2: + self.device.SetGain(2, 12) + elif n == 3: + self.device.SetGain(1, 12) + else: + print ('Unknown RfGain') + def ChangeFrequency(self, tune, vfo, source='', band='', event=None): + if vfo: + self.device.SetFrequency(vfo - self.transverter_offset) + return tune, vfo + def ChangeBand(self, band): + # band is a string: "60", "40", "WWV", etc. + BaseHardware.ChangeBand(self, band) + btn = self.application.BtnRfGain + if btn: + if band in ('160', '80', '60', '40'): + btn.SetLabel('RF +10', True) + elif band in ('20',): + btn.SetLabel('RF +20', True) + else: + btn.SetLabel('RF +20', True) + def VarDecimGetChoices(self): # return text labels for the control + l = [] # a list of sample rates + for dec in self.decimations: + l.append(str(int(float(self.clock) / dec / 1e3 + 0.5))) + return l + def VarDecimGetLabel(self): # return a text label for the control + return "Sample rate ksps" + def VarDecimGetIndex(self): # return the current index + return self.index + def VarDecimSet(self, index=None): # set decimation, return sample rate + if index is None: # initial call to set decimation before the call to open() + rate = self.application.vardecim_set # May be None or from different hardware + try: + dec = int(float(self.clock / rate + 0.5)) + self.index = self.decimations.index(dec) + except: + try: + self.index = self.decimations.index(self.conf.sdriq_decimation) + except: + self.index = 0 + else: + self.index = index + dec = self.decimations[self.index] + self.device.SetDecimation(dec) + if index is not None: + wx.BeginBusyCursor() + self.busy_cursor = True + return int(float(self.clock) / dec + 0.5) + def HeartBeat(self): + if self.busy_cursor: + if self.device.sdriq_decimation == self.device.new_decimation: + wx.EndBusyCursor() + self.busy_cursor = False + def StartSamples(self): # called by the sound thread + self.device.StartSamples() + def GetRxSamples(self): # called by the sound thread + # Quisk will call this frequently from the sound thread. Call AddRxSamples() to return the samples. + self.device.GetRxSamples() + def StopSamples(self): # called by the sound thread + self.device.StopSamples() + +# This class provides access to the SDR-IQ by RfSpace. +class Sdriq: + TYPE_HOST_SET = 0x00 + TYPE_HOST_GET = 0x20 + SDRIQ_READ_TIME = 0.004 # Number of seconds to wait for SDR-IQ data on each read + def __init__(self, hardware, name, clock, decim): + self.hardware = hardware + self.sdriq_name = name # port name such as "/dev/ttyUSB0" or "COM6" + self.sdriq_clock = clock + self.sdriq_decimation = decim # currently programmed decimation + self.new_decimation = decim # new requested decimation + self.sdriq_idle = -1 + self.port = None + self.sdriq_gstate = self.new_gstate = 2 + self.sdriq_gain = self.new_gain = 127 + self.sdriq_freq = self.new_freq = 7220000 + def open(self): + self.sdr_name = '' # name as reported by the hardware + self.sdr_serial = '' # serial number as reported by the hardware + self.sdr_data = bytearray(0) # the data block sent by the SDR-IQ + self.sdr_state = 0 + if not serial: + self.port = None + return 'SDR-IQ requires the missing Python "serial" module' + try: + self.port = serial.Serial(self.sdriq_name, baudrate=230400, timeout=self.SDRIQ_READ_TIME) + except: + if DEBUG: + traceback.print_exc() + self.port = None + return "Can not open SDR-IQ port name %s" % self.sdriq_name + self.SetItem(0x0018, b"\x81\x01\x00\x00") + self.port.reset_input_buffer() + self.port.reset_output_buffer() + self.SetItem(0x0018, b"\x81\x01\x00\x00") + self.GetItem(0x0002, b'') # request serial number + self.GetItem(0x0005, b'') # request status + self.GetItem(0x0001, b'') # request name + for i in range(50): + self.ReadSdriq() # read the port for data from the GUI thread + if self.sdr_name: + break + else: + self.port.close() + self.port = None + return "No response from SDR-IQ" + # set the clock speed + freq = self.sdriq_clock + data = bytearray(5) + data[0] = 0 + data[1] = freq & 0xFF + freq = freq >> 8 + data[2] = freq & 0xFF + freq = freq >> 8 + data[3] = freq & 0xFF + freq = freq >> 8 + data[4] = freq & 0xFF + self.SetItem(0x00B0, data) + t = "Capture from %s serial %s" % (self.sdr_name, self.sdr_serial) + if DEBUG: print("Open device:", t) + self.ProgramAD6620() + return t + def close(self): + if self.port: + self.port.close() + self.port = None + def StartSamples(self): + if self.sdriq_idle != 2 and self.port: + buf = bytearray(4) + buf[0] = 0x81 + buf[1] = 0x02 + buf[2] = 0x00 + buf[3] = 0x01 + self.SetItem(0x0018, buf) + if DEBUG: print ("StartSamples") + def StopSamples(self): + if not self.port: + return + if DEBUG: print ("StopSamples") + buf = bytearray(4) + buf[0] = 0x81 + buf[1] = 0x01 + buf[2] = 0x00 + buf[3] = 0x00 + for i in range(10): + self.SetItem(0x0018, buf) + self.ReadSdriq() + if self.sdriq_idle == 1: + if DEBUG: print ("StopSamples at index", i) + break + def SetDecimation(self, decim): + self.new_decimation = decim + def SetFrequency(self, freq): + self.new_freq = freq + def SetGain(self, gstate, gain): + self.new_gstate = gstate + self.new_gain = gain + def GetItem(self, item, params): + length = 4 + len(params) + data = bytearray(4) + data[0] = length & 0xFF # length LSB + data[1] = self.TYPE_HOST_GET | ((length >> 8) & 0x1F) # 3-bit type and 5-bit length MSB + data[2] = item & 0xFF # item LSB + data[3] = (item >> 8) & 0xFF # item MSB + data = data + params + if self.port.write(data) != length: + self.hardware.GotReadError(DEBUG, "SDR-IQ GetItem write error") + def SetItem(self, item, params): + length = 4 + len(params) + data = bytearray(4) + data[0] = length & 0xFF # length LSB + data[1] = self.TYPE_HOST_SET | ((length >> 8) & 0x1F) # 3-bit type and 5-bit length MSB + data[2] = item & 0xFF # item LSB + data[3] = (item >> 8) & 0xFF # item MSB + data = data + params + if self.port.write(data) != length: + self.hardware.GotReadError(DEBUG, "SDR-IQ SetItem write error") + def ProgramFrequency(self): + freq = self.sdriq_freq + buf = bytearray(6) + buf[0] = 0 + buf[1] = freq & 0xFF + freq = freq >> 8 + buf[2] = freq & 0xFF + freq = freq >> 8 + buf[3] = freq & 0xFF + freq = freq >> 8 + buf[4] = freq & 0xFF + buf[5] = 1 + self.SetItem(0x0020, buf) + def ProgramGain(self): + gain = self.sdriq_gain + gstate = self.sdriq_gstate + buf = bytearray(2) + if gstate == 0: + buf[0] = 0 + buf[1] = gain & 0xFF + elif gstate == 1: + buf[0] = 1 + buf[1] = gain & 0x7F + buf[1] |= 0x80 + else: + buf[0] = 1 + buf[1] = gain & 0x7F + self.SetItem(0x0038, buf) + def GetRxSamples(self): # Check for changes in decimation, frequency or gain. + if not self.port: # Poll the device for samples. + return None + if self.sdriq_decimation != self.new_decimation: + if DEBUG: print ("Set decimation to", self.new_decimation, "currently", self.sdriq_decimation) + self.StopSamples() + self.ProgramAD6620() + self.StartSamples() + self.sdriq_decimation = self.new_decimation + if self.sdriq_freq != self.new_freq: + self.sdriq_freq = self.new_freq + self.ProgramFrequency() + if self.sdriq_gain != self.new_gain or self.sdriq_gstate != self.new_gstate: + self.sdriq_gain = self.new_gain + self.sdriq_gstate = self.new_gstate + self.ProgramGain() + self.ReadSdriq() + def ReadSdriq(self): # Read all data from the SDR-IQ and process it. + # The ft245 driver does not have a circular buffer for input; bytes are just appended + # to the buffer. When all bytes are read and the buffer goes empty, the pointers are reset to zero. + # Be sure to empty out the ft245 frequently so its buffer does not overflow. + if not self.port: + return + data = self.port.read(8192) # this is a blocking read for SDRIQ_READ_TIME seconds + if isinstance(data, Q3StringTypes): + data = bytearray(data) + index = 0 + length = len(data) + while index < length: + if self.sdr_state == 0: # read the first byte + del self.sdr_data[:] + byte = data[index] + index += 1 + self.sdr_length = byte + self.sdr_state = 1 + elif self.sdr_state == 1: # read the second byte + byte = data[index] + index += 1 + self.sdr_type = (byte >> 5) & 0x7 # 3-bit type + self.sdr_length |= (byte & 0x1F) << 8 # length including header + if self.sdr_length == 0: + if self.sdr_type > 3: # special length + self.sdr_length = 8194 + else: # NAK + self.sdr_nak = 1 + self.sdr_state = 0 + continue + self.sdr_length -= 2 + if self.sdr_length <= 0 or (self.sdr_length > 50 and self.sdr_length < 8192): # out of sync + self.hardware.GotReadError(DEBUG, "SDR-IQ lost sync: type %d length %d" % (self.sdr_type, self.sdr_length)) + self.sdr_state = 9 + else: + self.sdr_state = 2 + elif self.sdr_state == 2: # read all the "sdr_length" bytes + index2 = index + self.sdr_length - len(self.sdr_data) + self.sdr_data += data[index:index2] + index = index2 + if len(self.sdr_data) >= self.sdr_length: # we have all the data for this record + self.sdr_state = 0 + if DEBUG > 1: + print("Got data type %d length %d" % (self.sdr_type, self.sdr_length)) + if self.sdr_length == 1 and self.sdr_type == 3: # ACK + self.sdr_ack = self.sdr_data[0] + elif self.sdr_type < 2 and self.sdr_length >= 2: # control item + item = self.sdr_data[0] | self.sdr_data[1] << 8 + if item == 1: + self.sdr_name = self.sdr_data[2:-1].decode('utf-8') + elif item == 2: + self.sdr_serial = self.sdr_data[2:-1].decode('utf-8') + elif item == 3: + self.sdr_interface = self.sdr_data[3] << 8 | self.sdr_data[2] + elif item == 4: + if self.sdr_data[2]: + self.sdr_firmware = self.sdr_data[4] << 8 | self.sdr_data[3] + else: + self.sdr_bootcode = self.sdr_data[4] << 8 | self.sdr_data[3] + elif item == 5: + self.sdr_status = self.sdr_data[2] + if self.sdr_status == 0x20: + self.hardware.GotClip() + elif item == 0x18: + self.sdriq_idle = self.sdr_data[3] + if (DEBUG): print("sdriq_idle", self.sdriq_idle) + elif self.sdr_type == 4 and self.sdr_length == 8192: # ADC sample block + self.hardware.AddRxSamples(self.sdr_data) + elif self.sdr_state == 9: # out of sync; try to re-synchronize + # look for the start of data blocks "\x00\x80" + byte = data[index] + index += 1 + if byte == 0x00: + self.sdr_state = 10 + elif self.sdr_state == 10: + byte = data[index] + index += 1 + if byte == 0x80: + del self.sdr_data[:] + self.sdr_length = 8192 + self.sdr_state = 2 + elif byte != 0x00: + self.sdr_state = 9 + def SetAD6620(self, address, value): # set an AD6620 register + buf = bytearray(9) + buf[0] = 0x09 + buf[1] = 0xA0 + buf[2] = address & 0xFF + buf[3] = (address >> 8) & 0xFF + buf[4] = value & 0xFF + value = value >> 8 + buf[5] = value & 0xFF + value = value >> 8 + buf[6] = value & 0xFF + value = value >> 8 + buf[7] = value & 0xFF + buf[8] = 0 + if self.port.write(buf) != len(buf): + self.hardware.GotReadError(DEBUG, "SDR-IQ SetAD6620 write error") + def WsetAD6620(self, address, value): # set an AD6620 register and wait for the ACK + self.sdr_ack = -1 + self.SetAD6620(address, value) + for i in range(50): + self.ReadSdriq() + if self.sdr_ack != -1: + break + if self.sdr_ack != 1: + self.hardware.GotReadError(DEBUG, "SDR-IQ failed to get ACK for AD6620 address 0x%X" % address) + def ProgramAD6620(self): + decim = self.new_decimation + if decim == 360: + scale = (4, 18, 5, 4, 13, 6) + coef = ( +131, -230, -38, -304, -235, -346, -237, -181, 12, 149, 310, 349, 320, 154, -60, +-310, -480, -540, -423, -169, 187, 523, 749, 762, 543, 117, -394, -851, -1093, -1025, +-621, 22, 737, 1300, 1522, 1288, 625, -309, -1245, -1893, -2013, -1515, -489, 793, 1957, + 2623, 2533, 1640, 149, -1533, -2893, -3475, -3023, -1584, 480, 2582, 4063, 4405, 3401, 1246, +-1484, -3986, -5455, -5345, -3557, -509, 2951, 5776, 7030, 6193, 3355, -760, -4970, -7969, -8722, +-6815, -2628, 2712, 7632, 10563, 10431, 7033, 1169, -5529, -11037, -13543, -12021, -6623, 1287, 9443, + 15320, 16896, 13319, 5269, -5122, -14811, -20711, -20642, -14088, -2504, 10961, 22272, 27682, 24909, 13986, +-2524, -20051, -33214, -37378, -30153, -12380, 11742, 35506, 51387, 53179, 38008, 7662, -31208, -68176, -91255, +-89756, -57102, 7096, 96306, 197916, 295555, 372388, 414662, 414662, 372388, 295555, 197916, 96306, 7096, -57102, +-89756, -91255, -68176, -31208, 7662, 38008, 53179, 51387, 35506, 11742, -12380, -30153, -37378, -33214, -20051, +-2524, 13986, 24909, 27682, 22272, 10961, -2504, -14088, -20642, -20711, -14811, -5122, 5269, 13319, 16896, + 15320, 9443, 1287, -6623, -12021, -13543, -11037, -5529, 1169, 7033, 10431, 10563, 7632, 2712, -2628, +-6815, -8722, -7969, -4970, -760, 3355, 6193, 7030, 5776, 2951, -509, -3557, -5345, -5455, -3986, +-1484, 1246, 3401, 4405, 4063, 2582, 480, -1584, -3023, -3475, -2893, -1533, 149, 1640, 2533, + 2623, 1957, 793, -489, -1515, -2013, -1893, -1245, -309, 625, 1288, 1522, 1300, 737, 22, +-621, -1025, -1093, -851, -394, 117, 543, 762, 749, 523, 187, -169, -423, -540, -480, +-310, -60, 154, 320, 349, 310, 149, 12, -181, -237, -346, -235, -304, -38, -230, 131) + elif decim == 500: + scale = (4, 25, 5, 4, 16, 5) + coef = ( +-197, 356, -153, 176, -101, 34, -125, -46, -106, -7, 12, 115, 129, + 157, 86, 12, -116, -197, -251, -203, -97, 80, 242, 364, 367, + 259, 33, -228, -461, -565, -504, -255, 106, 488, 756, 813, 604, + 172, -377, -868, -1139, -1066, -639, 53, 807, 1390, 1584, 1288, 537, +-470, -1439, -2046, -2060, -1406, -232, 1143, 2290, 2820, 2496, 1339, -366, +-2120, -3369, -3659, -2808, -976, 1340, 3448, 4652, 4486, 2873, 198, -2785, +-5152, -6095, -5184, -2546, 1137, 4785, 7240, 7613, 5604, 1641, -3190, -7438, +-9701, -9091, -5546, 69, 6163, 10849, 12519, 10373, 4745, -2905, -10342, -15198, +-15692, -11253, -2807, 7368, 16229, 20838, 19296, 11436, -946, -14436, -24891, -28637, +-23657, -10406, 8025, 26518, 39215, 41181, 30008, 6896, -23122, -51997, -70364, -69788, +-44995, 4465, 73600, 152608, 228689, 288639, 321648, 321648, 288639, 228689, 152608, 73600, + 4465, -44995, -69788, -70364, -51997, -23122, 6896, 30008, 41181, 39215, 26518, 8025, +-10406, -23657, -28637, -24891, -14436, -946, 11436, 19296, 20838, 16229, 7368, -2807, +-11253, -15692, -15198, -10342, -2905, 4745, 10373, 12519, 10849, 6163, 69, -5546, +-9091, -9701, -7438, -3190, 1641, 5604, 7613, 7240, 4785, 1137, -2546, -5184, +-6095, -5152, -2785, 198, 2873, 4486, 4652, 3448, 1340, -976, -2808, -3659, +-3369, -2120, -366, 1339, 2496, 2820, 2290, 1143, -232, -1406, -2060, -2046, +-1439, -470, 537, 1288, 1584, 1390, 807, 53, -639, -1066, -1139, -868, +-377, 172, 604, 813, 756, 488, 106, -255, -504, -565, -461, -228, + 33, 259, 367, 364, 242, 80, -97, -203, -251, -197, -116, 12, + 86, 157, 129, 115, 12, -7, -106, -46, -125, 34, -101, 176, -153, 356, -197) + elif decim == 600: + scale = (5, 30, 4, 5, 17, 5) + coef = ( + 436, -1759, 99, -1281, 0, -280, 619, 409, 553, -71, -344, -753, -537, -203, + 453, 782, 838, 325, -326, -949, -1037, -628, 230, 991, 1330, 923, 10, -1032, +-1569, -1324, -299, 956, 1822, 1739, 716, -809, -2000, -2212, -1212, 520, 2123, 2678, + 1823, -111, -2124, -3143, -2509, -463, 2002, 3548, 3279, 1188, -1699, -3877, -4088, +-2087, 1206, 4069, 4920, 3137, -478, -4094, -5720, -4343, -493, 3887, 6454, 5669, 1741, +-3412, -7052, -7096, -3266, 2607, 7462, 8573, 5084, -1425, -7602, -10058, -7187, -193, + 7400, 11481, 9579, 2301, -6756, -12777, -12244, -4971, 5569, 13854, 15181, 8285, -3699, +-14613, -18387, -12369, 966, 14920, 21888, 17412, 2905, -14598, -25744, -23754, -8362, + 13363, 30114, 32035, 16259, -10708, -35362, -43638, -28445, 5493, 42387, 62053, 49891, 5603, -53825, +-99044, -99811, -38467, 80479, 229234, 365232, 446270, 446270, 365232, 229234, 80479, -38467, +-99811, -99044, -53825, 5603, 49891, 62053, 42387, 5493, -28445, -43638, -35362, -10708, 16259, + 32035, 30114, 13363, -8362, -23754, -25744, -14598, 2905, 17412, 21888, 14920, 966, -12369, +-18387, -14613, -3699, 8285, 15181, 13854, 5569, -4971, -12244, -12777, -6756, 2301, 9579, + 11481, 7400, -193, -7187, -10058, -7602, -1425, 5084, 8573, 7462, 2607, -3266, -7096, -7052, -3412, + 1741, 5669, 6454, 3887, -493, -4343, -5720, -4094, -478, 3137, 4920, 4069, 1206, -2087, -4088, +-3877, -1699, 1188, 3279, 3548, 2002, -463, -2509, -3143, -2124, -111, 1823, 2678, 2123, 520, -1212, +-2212, -2000, -809, 716, 1739, 1822, 956, -299, -1324, -1569, -1032, 10, 923, 1330, 991, 230, -628, +-1037, -949, -326, 325, 838, 782, 453, -203, -537, -753, -344, -71, 553, 409, 619, -280, 0, -1281, + 99, -1759, 436) + else: # decim == 1250 + scale = (10, 25, 5, 7, 15, 6) + coef = ( +-378, 13756, -14444, 8014, -7852, 3556, -3779, 2733, -909, 2861, 208, 1827, -755, -243, -2134, -1267, -1705, + 20, 492, 2034, 1885, 1993, 535, -459, -2052, -2387, -2454, -1112, 246, 2053, 2832, 3019, 1774, 133, -1973, +-3220, -3654, -2546, -683, 1769, 3531, 4330, 3431, 1417, -1400, -3730, -5013, -4428, -2350, 831, 3780, 5669, + 5520, 3489, -23, -3635, -6252, -6689, -4839, -1057, 3245, 6715, 7904, 6403, 2443, -2555, -6998, -9129, -8175, +-4172, 1504, 7033, 10318, 10147, 6281, -23, -6747, -11415, -12315, -8815, -1972, 6041, 12354, 14669, 11830, 4593, +-4800, -13060, -17207, -15419, -7992, 2861, 13425, 19944, 19729, 12404, 21, -13318, -22930, -25017, -18239, -4245, + 12519, 26289, 31789, 26259, 10571, -10635, -30306, -41114, -38121, -20661, 6795, 35686, 55688, 58124, 39093, 1561, +-44548, -84372, -101901, -84500, -26969, 66196, 180937, 296484, 390044, 442339, 442339, 390044, 296484, 180937, + 66196, -26969, -84500, -101901, -84372, -44548, 1561, 39093, 58124, 55688, 35686, 6795, -20661, -38121, -41114, +-30306, -10635, 10571, 26259, 31789, 26289, 12519, -4245, -18239, -25017, -22930, -13318, 21, 12404, 19729, 19944, + 13425, 2861, -7992, -15419, -17207, -13060, -4800, 4593, 11830, 14669, 12354, 6041, -1972, -8815, -12315, -11415, +-6747, -23, 6281, 10147, 10318, 7033, 1504, -4172, -8175, -9129, -6998, -2555, 2443, 6403, 7904, 6715, 3245, -1057, +-4839, -6689, -6252, -3635, -23, 3489, 5520, 5669, 3780, 831, -2350, -4428, -5013, -3730, -1400, 1417, 3431, 4330, + 3531, 1769, -683, -2546, -3654, -3220, -1973, 133, 1774, 3019, 2832, 2053, 246, -1112, -2454, -2387, -2052, -459, + 535, 1993, 1885, 2034, 492, 20, -1705, -1267, -2134, -243, -755, 1827, 208, 2861, -909, 2733, -3779, 3556, -7852, + 8014, -14444, 13756, -378 ) + self.WsetAD6620(0x300, 1) + for i in range(256): + self.WsetAD6620(i, coef[i]) + self.WsetAD6620(0x301, 0) + self.WsetAD6620(0x302, -1) + self.WsetAD6620(0x303, 0) + self.WsetAD6620(0x304, 0) + self.WsetAD6620(0x305, scale[3]) + self.WsetAD6620(0x306, scale[0] - 1) + self.WsetAD6620(0x307, scale[4]) + self.WsetAD6620(0x308, scale[1] - 1) + self.WsetAD6620(0x309, scale[5]) + self.WsetAD6620(0x30A, scale[2] - 1) + self.WsetAD6620(0x30B, 0) + self.WsetAD6620(0x30C, 255) + self.WsetAD6620(0x30D, 0) + self.ProgramFrequency() + self.ProgramGain() + self.WsetAD6620(0x300, 0) diff --git a/quisk_utils.py b/quisk_utils.py new file mode 100644 index 0000000..4abe8a3 --- /dev/null +++ b/quisk_utils.py @@ -0,0 +1,70 @@ +from __future__ import print_function +from __future__ import division + +class SplineInterpolator: # From Numerical Recipes in C + """Interpolate a table of [x, y] values.""" + def __init__(self, table, Xmin=None, Xmax=None): + # The table is a list of [x, y]. Any interpolated x value must be within the table range. + # Do not mix [x, y] and (x, y). The table can be extended using Xmin and Xmax. + self.y2a = None + table.sort() # entries must be in order + for i in range(len(table) - 1, 0, -1): # entries must be unique + if table[i][0] == table[i - 1][0]: + del table[i] + if len(table) < 3: + return + yp1 = float(table[1][1] - table[0][1]) / (table[1][0] - table[0][0]) # first derivative at start + l = len(table) - 1 + ypn = float(table[l][1] - table[l - 1][1]) / (table[l][0] - table[l - 1][0]) # first derivative at end + if Xmin is not None and table[0][0] > Xmin: + Xmin = float(Xmin) + table.insert(0, [Xmin, table[0][1] - yp1 * (table[0][0] - Xmin)]) + l = len(table) - 1 + if Xmax is not None and table[l][0] < Xmax: + Xmax = float(Xmax) + table.append([Xmax, table[l][1] + ypn * (Xmax - table[l][0])]) + n = self.interp_n = len(table) + self.xa = x = [0.0] # index is 1, 2, ... + self.ya = y = [0.0] + for c, v in table: + x.append(float(c)) + y.append(float(v)) + u = [0] * n + y2 = self.y2a = [0] * (n + 1) + y2[1] = -0.5 + u[1] = (3.0 / (x[2] - x[1])) * ((y[2] - y[1]) / (x[2] - x[1]) -yp1) + for i in range(2, n): + sig = (x[i] - x[i-1]) / (x[i+1] - x[i-1]) + p = sig * y2[i-1] + 2.0 + y2[i] = (sig - 1.0) / p + u[i] = (y[i+1] - y[i]) / (x[i+1] - x[i]) - (y[i] - y[i-1]) / (x[i] - x[i-1]) + u[i] = (6.0 * u[i] / (x[i+1] - x[i-1]) - sig * u[i-1]) / p + qn = 0.5 + un = (3.0 / (x[n] - x[n-1])) * (ypn - (y[n] - y[n-1]) / (x[n] - x[n-1])) + y2[n] = (un - qn * u[n-1]) / (qn * y2[n-1] + 1.0) + for k in range(n-1, 0, -1): + y2[k] = y2[k] * y2[k+1] + u[k] + def Interpolate(self, x): + """Return the y value given x using spline interpolation.""" + if self.y2a is None: + return 0.0 + x = float(x) + n = self.interp_n + xa = self.xa + ya = self.ya + y2a = self.y2a + klo = 1 + khi = n + while (khi - klo) > 1: + k = (khi + klo) >> 1 + if xa[k] > x: + khi = k + else: + klo = k + h = xa[khi] - xa[klo] + if h == 0.0: + return 0.0 + a = (xa[khi] - x) / h + b = (x - xa[klo]) / h + y = a * ya[klo] + b * ya[khi] + ((a * a * a - a) * y2a[klo] + (b * b * b - b) * y2a[khi]) * (h * h) / 6.0 + return y diff --git a/quisk_vna.py b/quisk_vna.py new file mode 100644 index 0000000..cd23f57 --- /dev/null +++ b/quisk_vna.py @@ -0,0 +1,1423 @@ +#! /usr/bin/python + +# All QUISK software is Copyright (C) 2006-2018 by James C. Ahlstrom. +# This free software is licensed for use under the GNU General Public +# License (GPL), see http://www.opensource.org. +# Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!! + +"""The main program for Quisk VNA, a vector network analyzer. + +Usage: python quisk_vns.py [-c | --config config_file_path] +This can also be installed as a package and run as quisk_vna.main(). +""" + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import sys, os +os.chdir(os.path.normpath(os.path.dirname(__file__))) # change directory to the location of this script +if sys.path[0] != '': # Make sure the current working directory is on path + sys.path.insert(0, '') + +import wx, wx.html, wx.lib.stattext, wx.lib.colourdb +import math, cmath, time, traceback, string, pickle +import threading, webbrowser +import _quisk as QS +from quisk_widgets import * +import configure + +DEBUG = 0 + +# Command line parsing: be able to specify the config file. +from optparse import OptionParser +parser = OptionParser() +parser.add_option('-c', '--config', dest='config_file_path', + help='Specify the configuration file path') +parser.add_option('', '--config2', dest='config_file_path2', default='', + help='Specify a second configuration file to read after the first') +parser.add_option('-a', '--ask', action="store_true", dest='AskMe', default=False, + help='Ask which radio to use when starting') +argv_options = parser.parse_args()[0] +ConfigPath = argv_options.config_file_path # Get config file path +ConfigPath2 = argv_options.config_file_path2 +if sys.platform == 'win32': + path = os.getenv('HOMEDRIVE', '') + os.getenv('HOMEPATH', '') + for dir in ("Documents", "My Documents", "Eigene Dateien", "Documenti", "Mine Dokumenter"): + config_dir = os.path.join(path, dir) + if os.path.isdir(config_dir): + break + else: + config_dir = os.path.join(path, "My Documents") + try: + try: + import winreg as Qwinreg + except ImportError: + import _winreg as Qwinreg + key = Qwinreg.OpenKey(Qwinreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders") + val = Qwinreg.QueryValueEx(key, "Personal") + val = Qwinreg.ExpandEnvironmentStrings(val[0]) + Qwinreg.CloseKey(key) + if os.path.isdir(val): + DefaultConfigDir = val + else: + DefaultConfigDir = config_dir + except: + traceback.print_exc() + DefaultConfigDir = config_dir + if not ConfigPath: + ConfigPath = os.path.join(DefaultConfigDir, "quisk_conf.py") + if not os.path.isfile(ConfigPath): + path = os.path.join(config_dir, "quisk_conf.py") + if os.path.isfile(path): + ConfigPath = path + del config_dir +else: + DefaultConfigDir = os.path.expanduser('~') + if not ConfigPath: + ConfigPath = os.path.join(DefaultConfigDir, ".quisk_conf.py") + + +if not ConfigPath: # Use default path + if sys.platform == 'win32': + path = os.getenv('HOMEDRIVE', '') + os.getenv('HOMEPATH', '') + for dir in ("Documents", "My Documents", "Eigene Dateien", "Documenti"): + ConfigPath = os.path.join(path, dir) + if os.path.isdir(ConfigPath): + break + else: + ConfigPath = os.path.join(path, "My Documents") + ConfigPath = os.path.join(ConfigPath, "quisk_conf.py") + if not os.path.isfile(ConfigPath): # See if the user has a config file + try: + import shutil # Try to create an initial default config file + shutil.copyfile('quisk_conf_win.py', ConfigPath) + except: + pass + else: + ConfigPath = os.path.expanduser('~/.quisk_conf.py') + +class SoundThread(threading.Thread): + """Create a second (non-GUI) thread to read samples.""" + def __init__(self): + self.do_init = 1 + threading.Thread.__init__(self) + self.doQuit = threading.Event() + self.doQuit.clear() + def run(self): + """Read, process, play sound; then notify the GUI thread to check for FFT data.""" + if self.do_init: # Open sound using this thread + self.do_init = 0 + QS.start_sound() + wx.CallAfter(application.PostStartup) + while not self.doQuit.isSet(): + QS.read_sound() + wx.CallAfter(application.OnReadSound) + QS.close_sound() + def stop(self): + """Set a flag to indicate that the sound thread should end.""" + self.doQuit.set() + +class GraphDisplay(wx.Window): + """Display the graph within the graph screen.""" + def __init__(self, parent, x, y, graph_width, height, chary): + wx.Window.__init__(self, parent, + pos = (x, y), + size = (graph_width, height), + style = wx.NO_BORDER) + self.parent = parent + self.chary = chary + self.graph_width = graph_width + self.line_mag = [] + self.line_phase = [] + self.line_swr = [] + self.display_text = "" + self.SetBackgroundColour(conf.color_graph) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_LEFT_DOWN, parent.OnLeftDown) + self.Bind(wx.EVT_LEFT_UP, parent.OnLeftUp) + self.Bind(wx.EVT_MOTION, parent.OnMotion) + self.Bind(wx.EVT_MOUSEWHEEL, parent.OnWheel) + self.tune_tx = graph_width // 2 # Current X position of the Tx tuning line + self.height = 10 + self.y_min = 1000 + self.y_max = 0 + self.y_ticks = [] + self.max_height = application.screen_height + self.tuningPenTx = wx.Pen('Red', 1) + self.magnPen = wx.Pen('Black', 1) + self.phasePen = wx.Pen((0, 180, 0), 1) + self.swrPen = wx.Pen('Blue', 1) + self.backgroundPen = wx.Pen(self.GetBackgroundColour(), 1) + self.horizPen = wx.Pen(conf.color_gl, 1, wx.SOLID) + self.font = wx.Font(24, wx.FONTFAMILY_SWISS, wx.NORMAL, + wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + if sys.platform == 'win32': + self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter) + def OnEnter(self, event): + self.SetFocus() # Set focus so we get mouse wheel events + def OnPaint(self, event): + dc = wx.PaintDC(self) + x = self.tune_tx + dc.SetPen(self.tuningPenTx) + dc.DrawLine(x, 0, x, self.max_height) + dc.SetPen(self.horizPen) + for y in self.y_ticks: + dc.DrawLine(0, y, self.graph_width, y) + # Magnitude + t = 'Magnitude, ' + x = self.chary + y = self.height - self.chary + dc.SetTextForeground(self.magnPen.GetColour()) + dc.DrawText(t, x, y) + w, h = dc.GetTextExtent(t) + x += w + self.chary + # Phase + t = 'Phase, ' + dc.SetTextForeground(self.phasePen.GetColour()) + dc.DrawText(t, x, y) + w, h = dc.GetTextExtent(t) + x += w + self.chary + # SWR + t = 'SWR' + dc.SetTextForeground(self.swrPen.GetColour()) + dc.DrawText(t, x, y) + w, h = dc.GetTextExtent(t) + x += w + self.chary + # Draw graph + if self.line_phase: # Phase line + # Try to avoid drawing vertical lines when the phase goes from +180 to -180 + dc.SetPen(self.phasePen) + top = self.y_ticks[0] + high = self.y_ticks[1] + low = self.y_ticks[-2] + bottom = self.y_ticks[-1] + old_phase = self.line_phase[0] + line = [(0, old_phase)] + for x in range(1, self.graph_width): + phase = self.line_phase[x] + if phase < high and old_phase > low: + line.append((x-1, bottom)) + dc.DrawLines(line) + line = [(x, top), (x, phase)] + elif phase > low and old_phase < high: + line.append((x-1, top)) + dc.DrawLines(line) + line = [(x, bottom), (x, phase)] + else: + line.append((x, phase)) + old_phase = phase + dc.DrawLines(line) + if self.line_mag: # Magnitude line + dc.SetPen(self.magnPen) + dc.DrawLines(self.line_mag) + if self.line_swr: # SWR line + dc.SetPen(self.swrPen) + dc.DrawLines(self.line_swr) + if self.display_text: + dc.SetFont(self.font) + dc.SetTextBackground(conf.color_graph) + dc.SetTextForeground('red') + dc.SetBackgroundMode(wx.SOLID) + dc.DrawText(self.display_text, 10, 50) + def SetHeight(self, height): + self.height = height + self.SetSize((self.graph_width, height)) + def SetTuningLine(self, tune_tx): + dc = wx.ClientDC(self) + dc.SetPen(self.backgroundPen) + dc.DrawLine(self.tune_tx, 0, self.tune_tx, self.max_height) + dc.SetPen(self.tuningPenTx) + dc.DrawLine(tune_tx, 0, tune_tx, self.max_height) + self.tune_tx = tune_tx + self.Refresh() + +class GraphScreen(wx.Window): + """Display the graph screen X and Y axis, and create a graph display.""" + def __init__(self, parent, data_width, graph_width, correct_width, correct_delta, in_splitter=0): + wx.Window.__init__(self, parent, pos = (0, 0)) + self.in_splitter = in_splitter # Are we in the top of a splitter window? + self.data_width = data_width + self.graph_width = graph_width + self.correct_width = correct_width + self.correct_delta = correct_delta + self.started = False + self.doResize = False + self.pen_tick = wx.Pen("Black", 1, wx.SOLID) + self.font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + self.SetFont(self.font) + w = self.GetCharWidth() * 14 // 10 + h = self.GetCharHeight() + self.freq_start = 1000000 + self.freq_stop = 2000000 + self.charx = w + self.chary = h + self.mode = '' + self.data_mag = [] + self.data_phase = [] + self.data_impedance = [] + self.data_reflect = [] + self.data_freq = [0] * data_width + self.tick = max(2, h * 3 // 10) + self.originX = w * 5 + self.offsetY = h + self.tick + self.width = self.originX * 2 + self.graph_width + self.tick + self.height = application.screen_height * 3 // 10 + self.x0 = self.originX + self.graph_width // 2 # center of graph + self.originY = 10 + self.num_ticks = 8 # number of Y lines above the X axis + self.dy_ticks = 10 + # The pixel = slope * value + zero_pixel + # The value = (pixel - zero_pixel) / slope + self.leftZero = 10 # y location of left zero value + self.rightZero = 10 # y location of right zero value + self.leftSlope = 10 # slope of left scale times 360 + self.rightSlope = 10 # slope of right scale times 360 + self.SetSize((self.width, self.height)) + self.SetSizeHints(self.width, 1, self.width) + self.SetBackgroundColour(conf.color_graph) + self.Bind(wx.EVT_SIZE, self.OnSize) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) + self.Bind(wx.EVT_MOTION, self.OnMotion) + self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) + self.display = GraphDisplay(self, self.originX, 0, self.graph_width, 5, self.chary) + def OnPaint(self, event): + dc = wx.PaintDC(self) + if self.started and not self.in_splitter: + dc.SetFont(self.font) + self.MakeYTicks(dc) + self.MakeXTicks(dc) + def OnIdle(self, event): + if self.doResize: + self.ResizeGraph() + def OnSize(self, event): + self.doResize = True + self.ClearGraph() + event.Skip() + def ResizeGraph(self): + """Change the height of the graph. + + Changing the width interactively is not allowed. + Call after changing the zero or scale to recalculate the X and Y axis marks. + """ + w, h = self.GetClientSize() + if self.in_splitter: # Splitter window has no X axis scale + self.height = h + self.originY = h + else: + self.height = h - self.chary # Leave space for X scale + self.originY = self.height - self.offsetY + self.MakeYScale() + self.display.SetHeight(self.originY) + self.doResize = False + self.started = True + self.Refresh() + def MakeYScale(self): + chary = self.chary + dy = self.dy_ticks = (self.originY - chary * 2) // self.num_ticks # pixels per tick + ytot = dy * self.num_ticks + # Voltage dB scale + dbs = 80 # Number of dB to display + self.leftZero = self.originY - ytot - chary + self.leftSlope = - ytot * 360 // dbs # pixels per dB times 360 + # Phase scale + self.rightSlope = - ytot # pixels per degree times 360 + self.rightZero = self.originY - ytot // 2 - chary + # SWR scale + swrs = 9 # display range 1.0 to swrs + self.swrSlope = - ytot * 360 // (swrs - 1) # pixels per SWR unit times 360 + self.swrZero = self.originY - self.swrSlope // 360 - chary + def MakeYTicks(self, dc): + charx = self.charx + chary = self.chary + x1 = self.originX - self.tick * 3 # left of tick mark + x2 = self.originX - 1 # x location of left y axis + x3 = self.originX + self.graph_width # end of graph data + x4 = x3 + 1 # right y axis + x5 = x3 + self.tick * 3 # right tick mark + dc.SetPen(self.pen_tick) + dc.DrawLine(x2, 0, x2, self.originY + 1) # y axis + dc.DrawLine(x4, 0, x4, self.originY + 1) # y axis + del self.display.y_ticks[:] + y = self.leftZero + dc.SetTextForeground(self.display.magnPen.GetColour()) + for i in range(self.num_ticks + 1): + # Create the dB scale + val = (y - self.leftZero) * 360 // self.leftSlope + t = str(val) + dc.DrawLine(x1, y, x2, y) + self.display.y_ticks.append(y) + w, h = dc.GetTextExtent(t) + dc.DrawText(t, x1 - w, y - h // 2) + y += self.dy_ticks + y = self.leftZero + dc.SetTextForeground(self.display.phasePen.GetColour()) + for i in range(self.num_ticks + 1): + # Create the scale on the right + val = (y - self.rightZero) * 360 // self.rightSlope + t = str(val) + dc.DrawLine(x4, y, x5, y) + w, h = dc.GetTextExtent(t) + dc.DrawText(t, self.width - w - charx, y - h // 2 + 3) # right text + y += self.dy_ticks + # Create the SWR scale + if self.mode == 'Reflection': + y = self.leftZero + dc.SetTextForeground(self.display.swrPen.GetColour()) + for i in range(self.num_ticks + 1): + val = (y - self.swrZero) * 360 // self.swrSlope + t = str(val) + w, h = dc.GetTextExtent(t) + dc.DrawText(t, w//2, y - h // 2) + y += self.dy_ticks + def MakeXTicks(self, dc): + originY = self.originY + x3 = self.originX + self.graph_width # end of fft data + charx , z = dc.GetTextExtent('-30000XX') + tick0 = self.tick + tick1 = tick0 * 2 + tick2 = tick0 * 3 + dc.SetTextForeground(self.display.magnPen.GetColour()) + # Draw the X axis + dc.SetPen(self.pen_tick) + dc.DrawLine(self.originX, originY, x3, originY) + sample_rate = int(self.freq_stop - self.freq_start) + if sample_rate < 12000: + return + VFO = int((self.freq_start + self.freq_stop) / 2) + # Draw the band plan colors below the X axis + x = self.originX + f = float(x - self.x0) * sample_rate / self.data_width + c = None + y = originY + 1 + for freq, color in application.BandPlan: + freq -= VFO + if f < freq: + xend = int(self.x0 + float(freq) * self.data_width / sample_rate + 0.5) + if c is not None: + dc.SetPen(wx.TRANSPARENT_PEN) + dc.SetBrush(wx.Brush(c)) + dc.DrawRectangle(x, y, min(x3, xend) - x, tick0) # x axis + if xend >= x3: + break + x = xend + f = freq + c = color + stick = 1000 # small tick in Hertz + mtick = 5000 # medium tick + ltick = 10000 # large tick + # check the width of the frequency label versus frequency span + df = float(charx) * sample_rate / self.data_width # max label freq in Hertz + df *= 2.0 + df = math.log10(df) + expn = int(df) + mant = df - expn + if mant < 0.3: # label every 10 + tfreq = 10 ** expn + ltick = tfreq + mtick = ltick // 2 + stick = ltick // 10 + elif mant < 0.69: # label every 20 + tfreq = 2 * 10 ** expn + ltick = tfreq // 2 + mtick = ltick // 2 + stick = ltick // 10 + else: # label every 50 + tfreq = 5 * 10 ** expn + ltick = tfreq + mtick = ltick // 5 + stick = ltick // 10 + # Draw the X axis ticks and frequency in kHz + dc.SetPen(self.pen_tick) + freq1 = VFO - sample_rate // 2 + freq1 = (freq1 // stick) * stick + freq2 = freq1 + sample_rate + stick + 1 + y_end = 0 + for f in range (freq1, freq2, stick): + x = self.x0 + int(float(f - VFO) / sample_rate * self.data_width) + if self.originX <= x <= x3: + if f % ltick == 0: # large tick + dc.DrawLine(x, originY, x, originY + tick2) + elif f % mtick == 0: # medium tick + dc.DrawLine(x, originY, x, originY + tick1) + else: # small tick + dc.DrawLine(x, originY, x, originY + tick0) + if f % tfreq == 0: # place frequency label + t = str(f//1000) + w, h = dc.GetTextExtent(t) + dc.DrawText(t, x - w // 2, originY + tick2) + y_end = originY + tick2 + h + if y_end: # mark the center of the display + dc.DrawLine(self.x0, y_end, self.x0, application.screen_height) + def ClearGraph(self): + del self.display.line_mag[:] + del self.display.line_phase[:] + del self.display.line_swr[:] + del self.data_mag[:] + del self.data_phase[:] + del self.data_impedance[:] + del self.data_reflect[:] + self.display.Refresh() + def SetDisplayMsg(self, text=''): + self.display.display_text = text + self.display.Refresh() + def SetMode(self, mode): + self.mode = mode + def OnGraphData(self, volts): + # SWR = (1 + rho) / (1 - rho) + # Create graph lines + mode = self.mode + del self.display.line_mag[:] + del self.display.line_phase[:] + del self.display.line_swr[:] + del self.data_mag[:] + del self.data_phase[:] + del self.data_impedance[:] + del self.data_reflect[:] + if mode == 'Calibrate': + for x in range(application.correct_width): + self.calibrate_tmp[x] += volts[x] + self.calibrate_count += 1 + for x in range(self.graph_width): + self.data_impedance.append(50) + self.data_reflect.append(0) + i = x * self.correct_width // self.data_width + magn = abs(volts[i]) + phase = cmath.phase(volts[i]) * 360. / (2.0 * math.pi) + if magn < 1e-6: + db = -120.0 + else: + db = 20.0 * math.log10(magn) + self.data_mag.append(db) + y = self.leftZero - int( - db * self.leftSlope / 360.0 + 0.5) + self.display.line_mag.append((x, y)) + self.data_phase.append(phase) + y = self.rightZero - int( - phase * self.rightSlope / 360.0 + 0.5) + y = int(y) + self.display.line_phase.append(y) + elif mode == 'Reflection': + for x in range(self.graph_width): + delta = self.correct_delta + # Find the frequency for this pixel + freq = self.data_freq[x] + # Find the corresponding index into the correction array + i = int(freq / delta) + if i > self.correct_width - 2: + i = self.correct_width - 2 + dd = float(freq - i * delta) / delta # fractional part of next index for linear interpolation + Vx = volts[x] + # linear interpolation + if application.reflection_short is not None and application.reflection_open is not None and application.reflection_load is not None: + Vs = application.reflection_short[i] + (application.reflection_short[i+1] - application.reflection_short[i]) * dd + Vo = application.reflection_open[i] + (application.reflection_open[i+1] - application.reflection_open[i]) * dd + Vl = application.reflection_load[i] + (application.reflection_load[i+1] - application.reflection_load[i]) * dd + S11 = Vl + VVop = Vo - S11 + VVsh = Vs - S11 + try: + S12S21 = 2.0 * VVop * VVsh / (VVsh - VVop) + S22 = (VVop + VVsh) / (VVop - VVsh) + reflect = (Vx - S11) / (S12S21 + S22 * (Vx - S11)) + Z = 50.0 * (1.0 + reflect) / (1.0 - reflect) + except: + Z = 50E3 + reflect = (Z - 50) / (Z + 50) + #print ('Vs Vo Vl', abs(Vs), abs(Vo), abs(Vl), 'S22', abs(S22), 'S1221', abs(S12S21)) + else: + if application.reflection_open is not None: + correct = application.reflection_open[i] + (application.reflection_open[i+1] - application.reflection_open[i]) * dd + if application.reflection_short is not None: + correct = (correct - (application.reflection_short[i] + (application.reflection_short[i+1] - application.reflection_short[i]) * dd)) / 2.0 + else: # Use Short + correct = - (application.reflection_short[i] + (application.reflection_short[i+1] - application.reflection_short[i]) * dd) + try: + reflect = volts[x] / correct + Z = 50.0 * (1.0 + reflect) / (1.0 - reflect) + except: + Z = 50E3 + reflect = (Z - 50) / (Z + 50) + self.data_reflect.append(reflect) + self.data_impedance.append(Z) + magn = abs(reflect) + swr = (1.0 + magn) / (1.0 - magn) + if not 0.999 <= swr <= 99: + swr = 99.0 + if magn < 1e-6: + db = -120.0 + else: + db = 20.0 * math.log10(magn) + self.data_mag.append(db) + y = self.leftZero - int( - db * self.leftSlope / 360.0 + 0.5) + self.display.line_mag.append((x, y)) + phase = cmath.phase(reflect) * 360. / (2.0 * math.pi) + self.data_phase.append(phase) + y = self.rightZero - int( - phase * self.rightSlope / 360.0 + 0.5) + y = int(y) + self.display.line_phase.append(y) + y = self.swrZero - int( - swr * self.swrSlope / 360.0 + 0.5) + self.display.line_swr.append((x,y)) + else: # Mode is transmission + for x in range(self.graph_width): + delta = self.correct_delta + # Find the frequency for this pixel + freq = self.data_freq[x] + # Find the corresponding index into the correction array + i = int(freq / delta) + if i > self.correct_width - 2: + i = self.correct_width - 2 + dd = float(freq - i * delta) / delta # fractional part of next index for linear interpolation + trans = volts[x] + if application.transmission_open is not None: + trans -= application.transmission_open[i] + (application.transmission_open[i+1] - application.transmission_open[i]) * dd + trans /= application.transmission_short[i] + (application.transmission_short[i+1] - application.transmission_short[i]) * dd + self.data_reflect.append(trans) + self.data_impedance.append(50) + magn = abs(trans) + if magn < 1e-6: + db = -120.0 + else: + db = 20.0 * math.log10(magn) + self.data_mag.append(db) + y = self.leftZero - int( - db * self.leftSlope / 360.0 + 0.5) + self.display.line_mag.append((x, y)) + phase = cmath.phase(trans) * 360. / (2.0 * math.pi) + self.data_phase.append(phase) + y = self.rightZero - int( - phase * self.rightSlope / 360.0 + 0.5) + y = int(y) + self.display.line_phase.append(y) + self.display.Refresh() + def NewFreq(self, start, stop): + if self.freq_start != start or self.freq_stop != stop: + self.ClearGraph() + self.freq_start = start + self.freq_stop = stop + for i in range(self.data_width): # The frequency in Hertz for every graph pixel + self.data_freq[i] = int(start + float(stop - start) * i / (self.data_width - 1) + 0.5) + self.SetTxFreq(index=self.display.tune_tx) + self.doResize = True + def SetTxFreq(self, freq=None, index=None): + if index is None: + index = int(float(freq - self.freq_start) * (self.data_width - 1) / (self.freq_stop - self.freq_start) + 0.5) + if index < 0: + index = 0 + elif index >= self.data_width: + index = self.data_width - 1 + if freq is None: + freq = self.data_freq[index] + self.display.SetTuningLine(index) + application.ShowFreq(freq, index) + def GetMousePosition(self, event): + """For mouse clicks in our display, translate to our screen coordinates.""" + mouse_x, mouse_y = event.GetPosition() + win = event.GetEventObject() + if win is not self: + x, y = win.GetPosition().Get() + mouse_x += x + mouse_y += y + return mouse_x, mouse_y + def OnLeftDown(self, event): + mouse_x, mouse_y = self.GetMousePosition(event) + self.SetTxFreq(index=mouse_x - self.originX) + self.CaptureMouse() + def OnLeftUp(self, event): + if self.HasCapture(): + self.ReleaseMouse() + def OnMotion(self, event): + if event.Dragging() and event.LeftIsDown(): + mouse_x, mouse_y = self.GetMousePosition(event) + self.SetTxFreq(index=mouse_x - self.originX) + def OnWheel(self, event): + tune = self.display.tune_tx + event.GetWheelRotation() // event.GetWheelDelta() + self.SetTxFreq(index=tune) + +class HelpScreen(wx.html.HtmlWindow): + """Create the screen for the Help button.""" + def __init__(self, parent, width, height): + wx.html.HtmlWindow.__init__(self, parent, -1, size=(width, height)) + if "gtk2" in wx.PlatformInfo: + self.SetStandardFonts() + self.SetFonts("", "", [10, 12, 14, 16, 18, 20, 22]) + # read in text from file help.html in the directory of this module + self.LoadFile('help_vna.html') + def OnLinkClicked(self, link): + webbrowser.open(link.GetHref(), new=2) + +class QMainFrame(wx.Frame): + """Create the main top-level window.""" + def __init__(self, width, height): + fp = open('__init__.py') # Read in the title + self.title = fp.readline().strip() + fp.close() + self.title = 'Quisk Vector Network Analyzer ' + self.title[7:] + wx.Frame.__init__(self, None, -1, self.title, wx.DefaultPosition, + (width, height), wx.DEFAULT_FRAME_STYLE, 'MainFrame') + self.SetBackgroundColour(conf.color_bg) + self.Bind(wx.EVT_CLOSE, self.OnBtnClose) + def OnBtnClose(self, event): + application.OnBtnClose(event) + self.Destroy() + def SetConfigText(self, text): + if len(text) > 100: + text = text[0:80] + '|||' + text[-17:] + self.SetTitle("Radio %s %s %s" % (application.local_conf.RadioName, self.title, text)) + +class Spacer(wx.Window): + """Create a bar between the graph screen and the controls""" + def __init__(self, parent): + wx.Window.__init__(self, parent, pos = (0, 0), + size=(-1, 6), style = wx.NO_BORDER) + self.Bind(wx.EVT_PAINT, self.OnPaint) + r, g, b = parent.GetBackgroundColour().Get(False) + dark = (r * 7 // 10, g * 7 // 10, b * 7 // 10) + light = (r + (255 - r) * 5 // 10, g + (255 - g) * 5 // 10, b + (255 - b) * 5 // 10) + self.dark_pen = wx.Pen(dark, 1, wx.SOLID) + self.light_pen = wx.Pen(light, 1, wx.SOLID) + self.width = application.screen_width + def OnPaint(self, event): + dc = wx.PaintDC(self) + w = self.width + dc.SetPen(self.dark_pen) + dc.DrawLine(0, 0, w, 0) + dc.DrawLine(0, 1, w, 1) + dc.DrawLine(0, 2, w, 2) + dc.SetPen(self.light_pen) + dc.DrawLine(0, 3, w, 3) + dc.DrawLine(0, 4, w, 4) + dc.DrawLine(0, 5, w, 5) + +class CalibrateDialog(wx.Dialog): + def __init__(self, app): + self.app = app + self.correct_open = None + self.correct_short = None + self.correct_load = None + w, h = app.main_frame.GetSize().Get() + width = w // 2 + if app.screen_name == "Reflection": + title = "Calibrate for Reflection Mode" + t = '' + if app.reflection_short is not None: + t += "Short" + if app.reflection_open is not None: + t += "Open" + if app.reflection_load is not None: + t += "Load" + if t: + t = "Reflection mode calibration is %s from %s" % (t, app.calibrate_time) + else: + t = "Reflection mode is Uncalibrated" + else: + title = "Calibrate for Transmission Mode" + t = '' + if app.transmission_short is not None: + t += "Short" + if app.transmission_open is not None: + t += "Open" + if t: + t = "Transmission mode calibration is %s from %s" % (t, app.calibrate_time) + else: + t = "Transmission mode is Uncalibrated" + wx.Dialog.__init__(self, None, -1, title, size=(width, h)) + tab = self.GetCharHeight() * 2 + y = tab + txt = wx.StaticText(self, -1, t, pos=(tab, y)) + z, chary = txt.GetSize().Get() + y += chary * 3 // 2 + if app.screen_name == "Reflection": + t = "To calibrate the VNA for reflection mode, connect the standard Short, Open and Load connectors to the unknown port, and press the button." + t += " Reflection mode requires at least an Open or Short calibration, but using all three is highly recommended." + else: + t = "To calibrate the VNA for transmission mode, connect the cables together for Short, or leave them unconnected for Open, and press the button." + t += " The Short calibration is required, but the Open calibration is optional." + t += " The calibration will be saved for use the next time the program starts." + txt = wx.StaticText(self, -1, t, pos=(tab, y)) + txt.Wrap(width - tab * 2) + w, h = txt.GetSize().Get() + y += h + chary + # Calibrate buttons + t1 = wx.StaticText(self, -1, "Connect the Short connector and press", pos=(tab, y)) + tw, th = t1.GetSize().Get() + bx = tab + tw + tab // 2 + b1 = QuiskPushbutton(self, self.OnBtnShort, " Short ") + b1.SetColorGray() + bw, bh = b1.GetSize().Get() + by = y + (th - bh) // 2 + b1.Move(wx.Point(bx, by)) + self.txt_short = wx.StaticText(self, -1, "Not done", pos=(bx + bw + tab // 2, y)) + y = by + bh * 15 // 10 + by = y + (th - bh) // 2 + t2 = wx.StaticText(self, -1, "Connect the Open connector and press", pos=(tab, y), size = (tw, th)) + b2 = QuiskPushbutton(self, self.OnBtnOpen, "Open") + b2.SetColorGray() + b2.SetPosition((bx, by)) + b2.SetSize((bw, bh)) + self.txt_open = wx.StaticText(self, -1, "Not done", pos=(bx + bw + tab // 2, y)) + y = by + bh * 15 // 10 + by = y + (th - bh) // 2 + if app.screen_name == "Reflection": + t3 = wx.StaticText(self, -1, "Connect the Load connector and press", pos=(tab, y), size = (tw, th)) + b3 = QuiskPushbutton(self, self.OnBtnLoad, "Load") + b3.SetColorGray() + b3.SetPosition((bx, by)) + b3.SetSize((bw, bh)) + self.txt_load = wx.StaticText(self, -1, "Not done", pos=(bx + bw + tab // 2, y)) + y = by + bh * 15 // 10 + # Calibrate buttons + b1 = QuiskPushbutton(self, self.OnBtnCalibrate, " Calibrate ") + b1.SetColorGray() + b1.Enable(False) + w, h = b1.GetSize().Get() + b2 = QuiskPushbutton(self, self.OnBtnCancel, "Cancel") + b2.SetColorGray() + b2.SetSize((w, h)) + ww = (width - w * 2 - 40) // 3 + b1.Move(wx.Point(ww, y)) + b2.Move(wx.Point(width - w - ww, y)) + y += h * 3 // 2 + self.SetClientSize(wx.Size(width, y)) + self.btns = [b1, b2] + # timer for calibrate buttons + self.calibrate_timer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.OnCalibrateTimer, self.calibrate_timer) + def OnBtnCalibrate(self, event): + app = self.app + if app.screen_name == "Reflection": + app.reflection_short = self.correct_short + app.reflection_open = self.correct_open + app.reflection_load = self.correct_load + elif app.screen_name == "Transmission": + app.transmission_short = self.correct_short + app.transmission_open = self.correct_open + app.calibrate_time = time.asctime() + app.EnableButtons() + app.SetCalText() + app.SaveState() + self.EndModal(4) + def OnBtnCancel(self, event): + self.EndModal(5) + def Calibrate(self): + for b in self.btns: + b.Enable(False) + self.app.Calibrate() + self.calibrate_timer.Start(3000, oneShot=True) + def OnBtnShort(self, event): + self.txt_short.SetLabel("Wait") + self.mode = "Short" + self.Calibrate() + def OnBtnOpen(self, event): + self.txt_open.SetLabel("Wait") + self.mode = "Open" + self.Calibrate() + def OnBtnLoad(self, event): + self.txt_load.SetLabel("Wait") + self.mode = "Load" + self.Calibrate() + def OnCalibrateTimer(self, event): + self.app.running = False + if self.app.has_SetVNA: + Hardware.SetVNA(key_down=0) + for b in self.btns: + b.Enable(True) + data = self.app.graph.calibrate_tmp + count = self.app.graph.calibrate_count + if count == 0: + if self.mode == "Short": + self.txt_short.SetLabel("Not done") + elif self.mode == "Open": + self.txt_open.SetLabel("Not done") + elif self.mode == "Load": + self.txt_load.SetLabel("Not done") + return + for i in range(application.correct_width): + data[i] /= count + if self.mode == "Short": + self.txt_short.SetLabel("Done") + self.correct_short = data + elif self.mode == "Open": + self.txt_open.SetLabel("Done") + self.correct_open = data + elif self.mode == "Load": + self.txt_load.SetLabel("Done") + self.correct_load = data + +class App(wx.App): + """Class representing the application.""" + StateNames = ['transmission_open', 'transmission_short', 'reflection_open', 'reflection_short', 'reflection_load', 'calibrate_time', + 'calibrate_version'] + def __init__(self): + global application + application = self + QS.AppStatus(1) + self.bottom_widgets = None + self.is_vna_program = None + if sys.stdout.isatty(): + wx.App.__init__(self, redirect=False) + else: + wx.App.__init__(self, redirect=True) + def OnInit(self): + """Perform most initialization of the app here (called by wxPython on startup).""" + wx.lib.colourdb.updateColourDB() # Add additional color names + import quisk_widgets # quisk_widgets needs the application object + quisk_widgets.application = self + del quisk_widgets + global conf # conf is the module for all configuration data + import quisk_conf_defaults as conf + setattr(conf, 'config_file_path', ConfigPath) + setattr(conf, 'DefaultConfigDir', DefaultConfigDir) + self.QuiskFilesDir = os.path.dirname(conf.settings_file_path) # directory for Quisk files + if not os.path.isdir(self.QuiskFilesDir): + self.QuiskFilesDir = DefaultConfigDir + if os.path.isfile(ConfigPath): # See if the user has a config file + setattr(conf, 'config_file_exists', True) + d = {} + d.update(conf.__dict__) # make items from conf available + exec(compile(open(ConfigPath).read(), ConfigPath, 'exec'), d) # execute the user's config file + if os.path.isfile(ConfigPath2): # See if the user has a second config file + exec(compile(open(ConfigPath2).read(), ConfigPath2, 'exec'), d) # execute the user's second config file + for k in d: # add user's config items to conf + v = d[k] + if k[0] != '_': # omit items starting with '_' + setattr(conf, k, v) + else: + setattr(conf, 'config_file_exists', False) + # Read in configuration from the selected radio + self.BandPlan = [] + if configure: self.local_conf = configure.Configuration(self, argv_options.AskMe) + if configure: self.local_conf.UpdateConf() + # Choose whether to use Unicode or text symbols + for k in ('sym_stat_mem', 'sym_stat_fav', 'sym_stat_dx', + 'btn_text_range_dn', 'btn_text_range_up', 'btn_text_play', 'btn_text_rec', 'btn_text_file_rec', + 'btn_text_file_play', 'btn_text_fav_add', + 'btn_text_fav_recall', 'btn_text_mem_add', 'btn_text_mem_next', 'btn_text_mem_del'): + if conf.use_unicode_symbols: + setattr(conf, 'X' + k, getattr(conf, 'U' + k)) + else: + setattr(conf, 'X' + k, getattr(conf, 'T' + k)) + MakeWidgetGlobals() + self.BtnRfGain = None + self.graph_freq = 7e6 + self.graph_index = 50 + self.transmission_open = None + self.transmission_short = None + self.reflection_open = None + self.reflection_short = None + self.reflection_load = None + self.reflection_cal = "Cal x" + self.transmission_cal = "Cal x" + self.calibrate_time = time.asctime() + self.calibrate_version = 1 + QS.set_params(quisk_is_vna=1) # Call this only if we are the VNA program + # Open hardware file + self.firmware_version = None + global Hardware + if configure and self.local_conf.GetHardware(): + pass + else: + if hasattr(conf, "Hardware"): # Hardware defined in config file + self.Hardware = conf.Hardware(self, conf) + hname = ConfigPath + else: + self.Hardware = conf.quisk_hardware.Hardware(self, conf) + hname = conf.quisk_hardware.__file__ + if hname[-3:] == 'pyc': + hname = hname[0:-1] + setattr(conf, 'hardware_file_name', hname) + if conf.quisk_widgets: + hname = conf.quisk_widgets.__file__ + if hname[-3:] == 'pyc': + hname = hname[0:-1] + setattr(conf, 'widgets_file_name', hname) + else: + setattr(conf, 'widgets_file_name', '') + Hardware = self.Hardware + # Initialization + if configure: self.local_conf.Initialize() + # get the screen size + x, y, self.screen_width, self.screen_height = wx.Display().GetGeometry() + self.Bind(wx.EVT_QUERY_END_SESSION, self.OnEndSession) + self.sample_rate = 48000 + self.timer = time.time() # A seconds clock + self.time0 = 0 # timer to display fields + self.clip_time0 = 0 # timer to display a CLIP message on ADC overflow + self.heart_time0 = self.timer # timer to call HeartBeat at intervals + self.running = False + self.startup = True + self.save_data = [] + self.frequency = 0 + self.main_frame = frame = QMainFrame(10, 10) + self.SetTopWindow(frame) + # Find the data width, the width of returned graph data. + width = self.screen_width * conf.graph_width + width = int(width) + self.data_width = width + # correct_delta is the spacing of correction points in Hertz + if conf.use_rx_udp == 10: # Hermes UDP protocol + self.max_freq = 30000000 # maximum calculation frequency + self.correct_width = self.data_width # number of data points in the correct arrays + else: + self.max_freq = 60000000 + self.correct_width = self.max_freq // 15000 + 4 + if hasattr(Hardware, 'SetVNA'): + self.has_SetVNA = True + start, stop = Hardware.SetVNA(vna_start=0, vna_stop=self.max_freq, vna_count=self.correct_width) + self.correct_delta = float(stop - start) / (self.correct_width - 1) + Hardware.SetVNA(vna_count=self.data_width) + else: + self.has_SetVNA = False + self.correct_delta = 1 + # Restore persistent program state + self.init_path = os.path.join(os.path.dirname(ConfigPath), '.quisk_vna_init.pkl') + try: + fp = open(self.init_path, "r") + d = pickle.load(fp) + fp.close() + for k in d: + v = d[k] + if k in self.StateNames: + setattr(self, k, v) + except: + pass #traceback.print_exc() + # Record the basic application parameters + if sys.platform == 'win32': + h = self.main_frame.GetHandle() + else: + h = 0 + QS.set_enable_bandscope(0) + # FFT size must equal the data_width so that all data points are returned! + wisdom_path = os.path.join(os.path.dirname(self.local_conf.StatePath), 'quisk_wisdom.cache') + QS.record_app(self, conf, self.data_width, self.data_width, self.data_width, + 1, self.sample_rate, h, wisdom_path) + # Make all the screens and hide all but one + self.graph = GraphScreen(frame, self.data_width, self.data_width, self.correct_width, self.correct_delta) + self.screen = self.graph + width = self.graph.width + self.help_screen = HelpScreen(frame, width, self.screen_height // 10) + self.help_screen.Hide() + # Make a vertical box to hold all the screens and the bottom rows + vertBox = self.vertBox = wx.BoxSizer(wx.VERTICAL) + frame.SetSizer(vertBox) + # Add the screens + vertBox.Add(self.graph, 1) + vertBox.Add(self.help_screen, 1) + # Add the spacer + vertBox.Add(Spacer(frame), 0, wx.EXPAND) + # Add the sizer for the buttons + szr1 = wx.BoxSizer(wx.HORIZONTAL) + vertBox.Add(szr1, 0, wx.EXPAND, 0) + # Make the buttons in row 1 + self.buttons1 = buttons1 = [] + self.screen_name = "Reflection" + self.graph.SetMode(self.screen_name) + b = RadioButtonGroup(frame, self.OnBtnScreen, (' Transmission ', 'Reflection', 'Help'), self.screen_name) + buttons1 += b.buttons + self.btn_run = b = QuiskCheckbutton(frame, self.OnBtnRun, 'Run') + buttons1.append(b) + self.btn_calibrate = b = QuiskPushbutton(frame, self.OnBtnCal, 'Calibrate..') + buttons1.append(b) + width = 0 + for b in buttons1: + w, height = b.GetMinSize() + if width < w: + width = w + for i in range(24, 8, -2): + font = wx.Font(i, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + frame.SetFont(font) + w, h = frame.GetTextExtent('Start ') + if h < height * 9 // 10: + break + for b in buttons1: + b.SetMinSize((width, height)) + # Frequency entry start and stop + t = wx.lib.stattext.GenStaticText(frame, -1, 'Start ') + t.SetFont(font) + t.SetBackgroundColour(conf.color_bg) + gap = max(2, height//8) + freq0 = t + e = wx.TextCtrl(frame, -1, '1', style=wx.TE_PROCESS_ENTER) + e.SetFont(font) + tw, z = e.GetTextExtent("xx30.333xxxxx") + e.SetMinSize((tw, height)) + e.SetBackgroundColour(conf.color_entry) + self.freq_start_ctrl = e + frame.Bind(wx.EVT_TEXT_ENTER, self.OnNewFreq, source=e) + frame.Bind(wx.EVT_TEXT, self.OnNewFreq, source=e) + t = wx.lib.stattext.GenStaticText(frame, -1, 'Stop ') + t.SetFont(font) + t.SetBackgroundColour(conf.color_bg) + freq2 = t + e = wx.TextCtrl(frame, -1, '30', style=wx.TE_PROCESS_ENTER) + e.SetFont(font) + e.SetMinSize((tw, height)) + e.SetBackgroundColour(conf.color_entry) + self.freq_stop_ctrl = e + frame.Bind(wx.EVT_TEXT_ENTER, self.OnNewFreq, source=e) + frame.Bind(wx.EVT_TEXT, self.OnNewFreq, source=e) + # Band buttons + ilst = [] + slst = [] + for l in conf.BandEdge: # Sort keys + if not (l in conf.bandLabels or l == '60'): + continue + try: + ilst.append((int(l), conf.BandEdge[l])) + except ValueError: # item is a string, not an integer + slst.append((l, conf.BandEdge[l])) + ilst.sort() + ilst.reverse() + slst.sort() + band = [] + width = 0 + for l in ilst + slst: + b = QuiskPushbutton(frame, self.OnBtnBand, str(l[0])) + b.bandEdge = l[1] + band.append(b) + w, h= b.GetMinSize() + if width < w: + width = w + # make a list of all buttons + self.buttons = buttons1 + band + # Add button row to sizer + gap = max(2, height // 8) + gap2 = max(2, height // 4) + szr1.Add(buttons1[0], 0, wx.RIGHT|wx.LEFT, gap) + szr1.Add(buttons1[1], 0, wx.RIGHT, gap) + szr1.Add(buttons1[2], 0, wx.RIGHT, gap) + szr1.Add(buttons1[3], 0, wx.RIGHT|wx.LEFT, gap2) + szr1.Add(buttons1[4], 0, wx.RIGHT|wx.LEFT, gap) + szr1.Add(freq0, 0, wx.ALIGN_CENTER_VERTICAL) + szr1.Add(self.freq_start_ctrl, 0, wx.RIGHT, gap) + szr1.Add(freq2, 0, wx.ALIGN_CENTER_VERTICAL) + szr1.Add(self.freq_stop_ctrl, 0, wx.RIGHT, gap) + for x in band: + szr1.Add(x, 1, wx.RIGHT, gap) + self.statusbar = self.main_frame.CreateStatusBar() + # Set top window size + self.main_frame.SetClientSize(wx.Size(self.graph.width, self.screen_height * 5 // 10)) + w, h = self.main_frame.GetSize().Get() + self.main_frame.SetSizeHints(w, 1, w) + if hasattr(Hardware, 'pre_open'): # pre_open() is called before open() + Hardware.pre_open() + if conf.use_rx_udp == 10: # Hermes UDP protocol + self.add_version = False + conf.tx_ip = Hardware.hermes_ip + conf.tx_audio_port = conf.rx_udp_port + elif conf.use_rx_udp: + self.add_version = True # Add firmware version to config text + conf.rx_udp_decimation = 8 * 8 * 8 + if not conf.tx_ip: + conf.tx_ip = conf.rx_udp_ip + if not conf.tx_audio_port: + conf.tx_audio_port = conf.rx_udp_port + 2 + else: + self.add_version = False + # Open the hardware. This must be called before open_sound(). + self.config_text = Hardware.open() + self.status_error = "No hardware response" # possible error messages + if self.config_text: + self.main_frame.SetConfigText(self.config_text) + if conf.use_rx_udp == 10: # Hermes UDP protocol + if self.config_text[0:12] == "Capture from": + self.status_error = '' + else: + self.config_text = "Missing config_text" + # Note: Subsequent calls to set channels must not name a higher channel number. + # Normally, these calls are only used to reverse the channels. + index = 0 + QS.open_sound(0, conf.data_poll_usec, conf.latency_millisecs, + conf.tx_ip, conf.tx_audio_port, + conf.mic_sample_rate, conf.mic_channel_I, conf.mic_channel_Q, + 0.7, conf.mic_playback_rate) + self.Bind(wx.EVT_IDLE, self.graph.OnIdle) + frame.Show() + self.NewFreq(1000000, 30000000) + self.SetCalText() + self.WriteFields() + self.EnableButtons() + QS.set_fdx(1) + QS.set_rx_mode(0) + self.sound_thread = SoundThread() + self.sound_thread.start() + return True + def OnExit(self): + QS.close_rx_udp() + ##self.local_conf.SaveState() # to save default radio selection + return 0 + def SaveState(self): + if self.init_path: # save current program state + d = {} + for n in self.StateNames: + d[n] = getattr(self, n) + try: + fp = open(self.init_path, "w") + pickle.dump(d, fp) + fp.close() + except: + pass #traceback.print_exc() + def OnEndSession(self, event): + event.Skip() + self.OnBtnClose(event) + def OnBtnClose(self, event): + if self.has_SetVNA: + Hardware.SetVNA(key_down=0, do_tx=True) + time.sleep(0.5) + if self.sound_thread: + self.sound_thread.stop() + for i in range(0, 20): + if threading.activeCount() == 1: + break + time.sleep(0.1) + Hardware.close() + def OnBtnBand(self, event): + btn = event.GetEventObject() + start, stop = btn.bandEdge + start = float(start) * 1e-6 + stop = float(stop) * 1e-6 + self.freq_start_ctrl.SetValue(str(start)) + self.freq_stop_ctrl.SetValue(str(stop)) + def Calibrate(self): + self.graph.calibrate_tmp = [0] * self.correct_width + self.graph.calibrate_count = 0 + self.graph.SetMode("Calibrate") + self.NewFreq(0, self.max_freq) + if self.has_SetVNA: + Hardware.SetVNA(key_down=1) + self.running = True + self.startup = True + def OnBtnCal(self, event): + if self.has_SetVNA: + Hardware.SetVNA(key_down=0, vna_start=0, vna_stop=self.max_freq, vna_count=self.correct_width) + dlg = CalibrateDialog(self) + dlg.ShowModal() + dlg.Destroy() + if application.has_SetVNA: + Hardware.SetVNA(key_down=0, vna_count=self.data_width) + def OnBtnScreen(self, event): + btn = event.GetEventObject() + self.screen_name = btn.GetLabel().strip() + if self.screen_name == 'Help': + self.help_screen.Show() + self.graph.Hide() + else: + self.help_screen.Hide() + self.graph.Show() + self.graph.SetMode(self.screen_name) + self.vertBox.Layout() + self.EnableButtons() + def OnBtnRun(self, event): + btn = event.GetEventObject() + run = btn.GetValue() + if run: + for b in self.buttons1: + if b != btn: + b.Enable(False) + else: + for b in self.buttons1: + b.Enable(True) + self.graph.SetMode(self.screen_name) + if not self.running and not self.OnNewFreq(): + return + if self.has_SetVNA: + if run: + self.running = True + self.startup = True + Hardware.SetVNA(key_down=1) + else: + self.running = False + Hardware.SetVNA(key_down=0) + def EnableButtons(self): + if self.screen_name == 'Transmission': + if self.transmission_short is not None and len(self.transmission_short) == self.correct_width: + self.btn_run.Enable(1) + else: + self.btn_run.Enable(0) + elif self.screen_name == 'Reflection': + if (self.reflection_short is not None or self.reflection_open is not None) and len(self.reflection_short) == self.correct_width: + self.btn_run.Enable(1) + else: + self.btn_run.Enable(0) + else: # Help + self.btn_run.Enable(0) + def ShowFreq(self, freq, index): + self.frequency = freq + if hasattr(Hardware, 'ChangeFilterFrequency'): + Hardware.ChangeFilterFrequency(freq) + self.graph_freq = freq + self.graph_index = index + self.WriteFields() + def OnNewFreq(self, event=None): + if self.status_error and self.status_error[0:15] != "Error in Start ": + return False + try: + start = self.freq_start_ctrl.GetValue() + start = float(start) * 1e6 + stop = self.freq_stop_ctrl.GetValue() + stop = float(stop) * 1e6 + except: + self.status_error = "Error in Start or Stop freq" + #traceback.print_exc() + return False + start = int(start + 0.5) + stop = int(stop + 0.5) + if start > stop: + self.status_error = "Error in Start or Stop freq" + return False + if stop > self.max_freq: + stop = self.max_freq + self.freq_stop_ctrl.SetValue("%.6f" % (stop * 1.E-6)) + self.status_error = '' + self.NewFreq(start, stop) + return True + def NewFreq(self, start, stop): + if application.has_SetVNA: + start, stop = Hardware.SetVNA(vna_start=start, vna_stop=stop) + self.graph.NewFreq(start, stop) + def SetCalText(self): + text = '' + if self.reflection_short is not None: + text += "S" + if self.reflection_open is not None: + text += "O" + if self.reflection_load is not None: + text += "L" + if text: + text = "Cal " + text + else: + text = "Cal x" + self.reflection_cal = text + text = '' + if self.transmission_short is not None: + text += "S" + if self.transmission_open is not None: + text += "O" + if text: + text = "Cal " + text + else: + text = "Cal x" + self.transmission_cal = text + def WriteFields(self): + index = self.graph_index + if index < 0: + index = 0 + elif index >= self.data_width: + index = self.data_width - 1 + freq = "Freq %.6f" % (self.frequency * 1E-6) + mode = self.graph.mode + if self.status_error: + text = self.status_error + elif not self.graph.data_mag: + if mode == 'Transmission': + text = u" %s %s" % (self.transmission_cal, freq) + elif mode == 'Reflection': + text = u" %s %s" % (self.reflection_cal, freq) + else: + text = '' + elif mode == 'Calibrate': + db = self.graph.data_mag[index] + phase = self.graph.data_phase[index] + text = u" %s Calibrate %.2f dB %.1f\u00B0" % (freq, db, phase) + elif mode == 'Transmission': + db = self.graph.data_mag[index] + phase = self.graph.data_phase[index] + text = u" %s %s Transmission %.2f dB %.1f\u00B0" % (self.transmission_cal, freq, db, phase) + elif mode == 'Reflection': + db = self.graph.data_mag[index] + phase = self.graph.data_phase[index] + aref = abs(self.graph.data_reflect[index]) + swr = (1.0 + aref) / (1.0 - aref) + if not 0.999 <= swr <= 99: + swr = 99.0 + text = u" %s %s Reflect ( %.2f dB %.1f\u00B0 ) SWR %.1f" % (self.reflection_cal, freq, db, phase, swr) + Z = self.graph.data_impedance[index] + mag = abs(Z) + phase = cmath.phase(Z) * 360. / (2.0 * math.pi) + freq = self.graph.data_freq[index] + z_real = Z.real + z_imag = Z.imag + if z_imag < 0: + text += u" Z \u03A9 ( %.1f - %.1fJ ) = ( %.1f %.1f\u00B0 )" % (z_real, abs(z_imag), mag, phase) + else: + text += u" Z \u03A9 ( %.1f + %.1fJ ) = ( %.1f %.1f\u00B0 )" % (z_real, z_imag, mag, phase) + if z_imag >= 0.5: + L = z_imag / (2.0 * math.pi * freq) * 1e9 + Xp = (z_imag ** 2 + z_real ** 2) / z_imag + Lp = Xp / (2.0 * math.pi * freq) * 1e9 + text += ' L %.0f nH' % L + if z_real > 0.01: + Rp = (z_imag ** 2 + z_real ** 2) / z_real + text += " ( %.1f || %.0f nH )" % (Rp, Lp) + elif z_imag < -0.5: + C = -1.0 / (2.0 * math.pi * freq * z_imag) * 1e9 + Xp = (z_imag ** 2 + z_real ** 2) / z_imag + Cp = -1.0 / (2.0 * math.pi * freq * Xp) * 1e9 + text += ' C %.3f nF' % C + if z_real > 0.01: + Rp = (z_imag ** 2 + z_real ** 2) / z_real + text += " ( %.1f || %.3f nF )" % (Rp, Cp) + self.statusbar.SetStatusText(text) + def PostStartup(self): # called once after sound attempts to start + pass + def OnReadSound(self): # called at frequent intervals + self.timer = time.time() + dat = QS.get_graph(0, 1.0, 0) + if dat and self.running: + dat = list(dat) + try: + start = dat.index(0) + except ValueError: + self.save_data += dat + return + data = self.save_data + dat[0:start] + self.save_data = dat[start+1:] + if self.graph.mode == 'Calibrate': + if len(data) != self.correct_width: + if DEBUG: print(' bad calibrate array', len(data), self.correct_width) + return + else: + if len(data) != self.data_width: + if DEBUG: print(' bad data array', len(data), self.data_width) + return + for i in range(len(data)): + data[i] /= 2147483647.0 + if self.startup: # always skip the first block of data + self.startup = False + else: + self.graph.OnGraphData(data) + if QS.get_overrange() and self.running: + self.clip_time0 = self.timer + self.status_error = " *** CLIP ***" + self.graph.SetDisplayMsg("Clip") + if self.clip_time0: + if self.timer - self.clip_time0 > 1.0: + self.clip_time0 = 0 + self.status_error = '' + self.graph.SetDisplayMsg() + if self.timer - self.heart_time0 > 0.10: # call hardware to perform background tasks + self.heart_time0 = self.timer + Hardware.HeartBeat() + if self.add_version and self.firmware_version is None: + self.firmware_version = Hardware.GetFirmwareVersion() + if self.firmware_version is not None: + if self.firmware_version < 3: + self.status_error = "Need firmware ver 3" + else: + self.status_error = '' + # Set text fields + if self.timer - self.time0 > 0.5: + self.time0 = self.timer + #print "len %5d re %9.6f im %9.6f mag %9.6f phase %7.2f" % (len(data), + # volts.real, volts.imag, abs(volts), phase) + #print "Z re %12.2f im %12.2f mag %12.2f phase %7.2f" % (zzz.real, zzz.imag, + # abs(zzz), cmath.phase(zzz) * 360. / (2.0 * math.pi)) + self.WriteFields() + +def main(): + """If quisk is installed as a package, you can run it with quisk.main().""" + App() + application.MainLoop() + +if __name__ == '__main__': + main() + diff --git a/quisk_wdsp.c b/quisk_wdsp.c new file mode 100644 index 0000000..91a33ed --- /dev/null +++ b/quisk_wdsp.c @@ -0,0 +1,84 @@ +// This module provides C access to the WDSP SDR library. + +#include +#include + +#define CLIP32 2147483647 +#define CLIP16 32767 + +#define MAX_CHANNELS 32 +#define SAMPLE_BYTES (sizeof(double) * 2) + +static struct _channel { + double * cBuf; // save samples until nBuf reaches in_size + int sizeBuf; // current size of cBuf in count of complex samples + int nBuf; // current number of complex samples in cBuf + int in_size; // count of complex samples needed for wdsp_fexchange0 + int in_use; // is this channel being used? +} wdspChannel[MAX_CHANNELS]; + +static void (*wdsp_fexchange0) (int channel, double * in, double * out, int * error); + +int wdspFexchange0(int channel, double * cSamples, int nSamples) +{ +// Call with an arbitrary number of input samples nSamples. cSamples is scaled to CLIP32. +// When we have enough samples process the samples by calling wdsp. Return the samples in the same array. +// cSamples is complex double. We treat it as interleaved Real/Imag doubles twice as long. +// nSamples is the count of complex samples. + struct _channel * ptChannel = wdspChannel + channel; + int i, error; + double * ptD, * ptS; + + if ( ! ptChannel->in_use) + return nSamples; + if ( ! wdsp_fexchange0) + return nSamples; + if (nSamples <= 0) + return nSamples; + if (ptChannel->nBuf + nSamples >= ptChannel->sizeBuf) { + ptChannel->sizeBuf = ptChannel->nBuf + nSamples * 3; + ptChannel->cBuf = (double *)realloc(ptChannel->cBuf, ptChannel->sizeBuf * SAMPLE_BYTES); + } + ptS = cSamples; // Copy samples from cSamples to cBuf + ptD = ptChannel->cBuf + ptChannel->nBuf * 2; + for (i = 0; i < nSamples; i++) { + *ptD++ = *ptS++ / CLIP32; + *ptD++ = *ptS++ / CLIP32; + } + ptChannel->nBuf += nSamples; + nSamples = 0; + while (ptChannel->nBuf >= ptChannel->in_size) { + (*wdsp_fexchange0)(channel, ptChannel->cBuf, cSamples + nSamples * 2, &error); + if (error) + printf("WDSP: wdsp_fexchange0 error %d\n", error); + nSamples += ptChannel->in_size; + ptChannel->nBuf -= ptChannel->in_size; + memmove(ptChannel->cBuf, ptChannel->cBuf + ptChannel->in_size * 2, ptChannel->nBuf * SAMPLE_BYTES); + } + for (i = 0; i < nSamples * 2; i++) + cSamples[i] *= CLIP32; + return nSamples; +} + +PyObject * quisk_wdsp_set_parameter(PyObject * self, PyObject * args, PyObject * keywds) // Called from the GUI thread. +{ // Call with channel and keyword arguments. Channel is required but may not be needed. + int channel; + int in_size = -1; + int in_use = -1; + intptr_t fexchange0 = 0; + static char * kwlist[] = {"channel", "in_size", "fexchange0", "in_use", NULL} ; + + if (!PyArg_ParseTupleAndKeywords (args, keywds, "i|iKi", kwlist, &channel, &in_size, &fexchange0, &in_use)) + return NULL; + if (channel >= 0 && channel < MAX_CHANNELS) { + if (fexchange0) + wdsp_fexchange0 = (void *)fexchange0; + if (in_size > 0) + wdspChannel[channel].in_size = in_size; + if (in_use >= 0) + wdspChannel[channel].in_use = in_use; + } + Py_INCREF (Py_None); + return Py_None; +} + diff --git a/quisk_wdsp.py b/quisk_wdsp.py new file mode 100644 index 0000000..dc7e68e --- /dev/null +++ b/quisk_wdsp.py @@ -0,0 +1,135 @@ +# This module provides Python access to the WDSP SDR library. + +import sys, ctypes, ctypes.util, queue, traceback +import _quisk as QS + +class Cwdsp: + def __init__(self, app): + self.Lib = None + self.version = 0 + if ctypes.sizeof(ctypes.c_voidp) != 8 or sys.version_info.major < 3: # Must be 64-bit Python3 + return + self.Log = Log = app.std_out_err.Logfile + Log("Start of wdsp") + if sys.platform == 'win32': + try: + self.Lib = ctypes.WinDLL(".\\libwdsp.dll") + Log ("Windows: Found private wdsp") + except: + name = ctypes.util.find_library("wdsp") + if name: + try: + self.Lib = ctypes.WinDLL(name) + Log ("Windows: Found public wdsp") + except: + pass + else: + try: + self.Lib = ctypes.CDLL("./libwdsp.so") + Log ("Found private wdsp") + except: + name = ctypes.util.find_library("wdsp") + if name: + try: + self.Lib = ctypes.CDLL(name) + Log ("Found public wdsp") + except: + pass + if not self.Lib: + Log("Wdsp was not found") + return + try: + func = self.Lib.GetWDSPVersion + except: + print("Failed to open WDSP: No version information") + self.Lib = None + return + try: + self.version = func() + except: + print("Failed to open WDSP: Call to version() failed") + self.Lib = None + return + Log ("Wdsp version %d" % self.version) + self.queue = queue.SimpleQueue() + try: + func = self.Lib.fexchange0 + vpt = ctypes.cast(func, ctypes.c_void_p) + fpt = vpt.value + except: + print("Failed to find fexchange0") + self.Lib = None + return + QS.wdsp_set_parameter(0, fexchange0=fpt) + Log ("Library wdsp is active") + def open(self, channel): + Lib = self.Lib + if not Lib: + return + self.Log("Open channel %d" % channel) + wisdom1 = QS.read_fftw_wisdom() + try: + in_size = 256 + dsp_size = 256 + QS.wdsp_set_parameter(channel, in_size=in_size) + Lib.OpenChannel (channel, in_size, dsp_size, 48000, 48000, 48000, 0, 1, + ctypes.c_double(0.010), ctypes.c_double(0.025), ctypes.c_double(0.0), ctypes.c_double(0.010), 0) + Lib.SetRXAShiftRun (channel, 0) + Lib.RXANBPSetRun (channel, 0) + Lib.SetRXAAMSQRun (channel, 0) + Lib.SetRXAMode (channel, 1) # USB + Lib.RXASetPassband (channel, ctypes.c_double(300.0), ctypes.c_double(3000.0)) + Lib.RXASetNC (channel, dsp_size) + Lib.RXASetMP (channel, 0) + Lib.SetRXAAGCMode(channel, 0) + Lib.SetRXAAGCFixed(channel, ctypes.c_double(0.0)) + Lib.SetRXAPanelRun(channel, 0) + Lib.SetRXAEMNRRun(channel, 0) + except: + traceback.print_exc() + self.Lib = None + self.version = 0 + return + wisdom2 = QS.read_fftw_wisdom() + if wisdom1 != wisdom2: + QS.write_fftw_wisdom() + def __getattr__(self, name): + if name.startswith('__') and name.endswith('__'): + raise AttributeError(name) + if not self.Lib: + return None + try: + func = self.Lib.__getattr__(name) + except: + print ("WDSP: Unknown function", name) + func = None + else: + setattr(self, name, func) + return func + def put(self, *args): # Called by the GUI thread + if not self.Lib: + return + self.queue.put(args) + def control(self): # Called by the sound thread + if not self.Lib: + return + if self.queue.empty(): + return + try: + item = self.queue.get_nowait() + except: + return + if not item[0]: + return + args = [] + for arg in item[1:]: + if isinstance(arg, int): + args.append(arg) + elif isinstance(arg, float): + args.append(ctypes.c_double(arg)) + elif isinstance(arg, str): + args.append(ctypes.c_char_p(arg.encode())) + else: + print ("WDSP: Unknown type of argument") + return + item[0](*tuple(args)) diff --git a/quisk_widgets.py b/quisk_widgets.py new file mode 100644 index 0000000..21fee2d --- /dev/null +++ b/quisk_widgets.py @@ -0,0 +1,1567 @@ +# These are Quisk widgets + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import sys, re, json +import wx, wx.lib.buttons, wx.lib.stattext +# The main script will alter quisk_conf_defaults to include the user's config file. +import quisk_conf_defaults as conf +import _quisk as QS + +wxVersion = wx.version()[0] + +def EmptyBitmap(width, height): + if wxVersion in ('2', '3'): + return wx.EmptyBitmap(width, height) + else: + return wx.Bitmap(width, height) + +def MakeWidgetGlobals(): + global button_font, button_uline_font, button_bezel, button_width, button_height, button_text_width, button_text_height + global _bitmap_menupop, _bitmap_sliderpop, _bitmap_cyclepop + button_bezel = 3 # size of button bezel in pixels + button_font = wx.Font(conf.button_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL, + wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + button_uline_font = wx.Font(conf.button_font_size, wx.FONTFAMILY_SWISS, wx.NORMAL, + wx.FONTWEIGHT_NORMAL, True, conf.quisk_typeface) + dc = wx.MemoryDC() + dc.SetFont(button_font) + tmp_bm = EmptyBitmap(1, 1) # Thanks to NS4Y + dc.SelectObject(tmp_bm) + button_text_width, button_text_height = dc.GetTextExtent('0') + button_width = button_text_width + 2 + 2 * button_bezel # + 4 * int(self.useFocusInd) + button_height = button_text_height + 2 + 2 * button_bezel # + 4 * int(self.useFocusInd) + # Make a bitmap for the slider pop button + height = button_text_height + 2 # button height less bezel + width = height + _bitmap_sliderpop = EmptyBitmap(height, height) + dc.SelectObject(_bitmap_sliderpop) + pen = wx.Pen(conf.color_enable, 1) + dc.SetPen(pen) + brush = wx.Brush(conf.color_btn) + dc.SetBackground(brush) + dc.Clear() + w = width * 5 // 10 + w += w % 2 + bd = (width - 1 - w) // 2 + x1 = bd + x2 = x1 + w + y1 = 0 + y2 = height - y1 - 1 + dc.DrawLine(x1, y1, x2, y1) + dc.DrawLine(x2, y1, x2, y2) + dc.DrawLine(x2, y2, x1, y2) + dc.DrawLine(x1, y2, x1, y1) + x0 = (x2 + x1) // 2 + dc.DrawLine(x0, y1 + 3, x0, y2 - 2) + y0 = height * 6 // 10 + dc.DrawLine(x0 - 2, y0, x0 + 3, y0) + y0 -= 1 + color = pen.GetColour() + r = color.Red() + g = color.Green() + b = color.Blue() + f = 160 + r = min(r + f, 255) + g = min(g + f, 255) + b = min(b + f, 255) + color = wx.Colour(r, g, b) + dc.SetPen(wx.Pen(color, 1, wx.SOLID)) + dc.DrawLine(x0 - 2, y0, x0 + 3, y0) + dc.SelectObject(wx.NullBitmap) + # Make a bitmap for the menu pop button + _bitmap_menupop = EmptyBitmap(height, height) + dc.SelectObject(_bitmap_menupop) + dc.SetBackground(brush) + dc.Clear() + dc.SetPen(wx.Pen(conf.color_enable, 1)) + dc.SetBrush(wx.Brush(conf.color_enable)) + x = 3 + for y in range(2, height - 3, 5): + dc.DrawRectangle(x, y, 3, 3) + dc.DrawLine(x + 5, y + 1, width - 3, y + 1) + dc.SelectObject(wx.NullBitmap) + # Make a bitmap for the cycle button + _bitmap_cyclepop = EmptyBitmap(height, height) + dc.SelectObject(_bitmap_cyclepop) + dc.SetBackground(brush) + dc.SetFont(button_font) + dc.Clear() + w, h = dc.GetTextExtent(conf.btn_text_cycle) + dc.DrawText(conf.btn_text_cycle, (height - x) // 2, (height - y) // 2) + dc.SelectObject(wx.NullBitmap) + +def FreqFormatter(freq): # Format the string or integer frequency by adding blanks + freq = int(freq) + if freq >= 0: + t = str(freq) + minus = '' + else: + t = str(-freq) + minus = '- ' + l = len(t) + if l > 9: + txt = "%s%s %s %s %s" % (minus, t[0:-9], t[-9:-6], t[-6:-3], t[-3:]) + elif l > 6: + txt = "%s%s %s %s" % (minus, t[0:-6], t[-6:-3], t[-3:]) + elif l > 3: + txt = "%s%s %s" % (minus, t[0:-3], t[-3:]) + else: + txt = minus + t + return txt + +class FrequencyDisplay(wx.lib.stattext.GenStaticText): + """Create a frequency display widget.""" + def __init__(self, frame, width, height): + wx.lib.stattext.GenStaticText.__init__(self, frame, -1, '3', + style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE) + border = 4 + for points in range(30, 6, -1): + font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + self.SetFont(font) + w, h = self.GetTextExtent('333 444 555 Hz') + if w < width and h < height - border * 2: + break + self.SetSizeHints(w, h, w * 5, h) + self.height = h + self.points = points + border = self.border = (height - self.height) // 2 + self.height_and_border = h + border * 2 + self.SetBackgroundColour(conf.color_freq) + self.SetForegroundColour(conf.color_freq_txt) + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) # Click on a digit changes the frequency + self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown) + self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) + self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) + self.timer = wx.Timer(self) # Holding a digit continuously changes the frequency + self.Bind(wx.EVT_TIMER, self.OnTimer) + self.repeat_time = 0 # Repeat function is inactive + def Clip(self, clip): + """Change color to indicate clipping.""" + if clip: + self.SetBackgroundColour('deep pink') + else: + self.SetBackgroundColour(conf.color_freq) + self.Refresh() + def Display(self, freq): + """Set the frequency to be displayed.""" + txt = FreqFormatter(freq) + self.SetLabel('%s Hz' % txt) + def GetIndex(self, event): # Determine which digit is being changed + mouse_x, mouse_y = event.GetPosition() + width, height = self.GetClientSize().Get() + text = self.GetLabel() + tw, th = self.GetTextExtent(text) + edge = (width - tw) // 2 + digit = self.GetTextExtent('0')[0] + blank = self.GetTextExtent(' ')[0] + if mouse_x < edge - digit: + return None + x = width - edge - self.GetTextExtent(" Hz")[0] - mouse_x + if x < 0: + return None + #print ('size', width, height, 'mouse', mouse_x, mouse_y, 'digit', digit, 'blank', blank) + shift = 0 + while x > digit * 3: + shift += 1 + x -= digit * 3 + blank + if x < 0: + return None + return x // digit + shift * 3 # index of digit being changed + def OnLeftDown(self, event): # Click on a digit changes the frequency + if self.repeat_time: + self.timer.Stop() + self.repeat_time = 0 + index = self.GetIndex(event) + if index is not None: + self.index = index + mouse_x, mouse_y = event.GetPosition() + width, height = self.GetClientSize().Get() + if mouse_y < height // 2: + self.increase = True + else: + self.increase = False + self.ChangeFreq() + self.repeat_time = 300 # first button push + self.timer.Start(milliseconds=300, oneShot=True) + def OnLeftUp(self, event): + self.timer.Stop() + self.repeat_time = 0 + def ChangeFreq(self): + text = self.GetLabel() + text = text.replace(' ', '')[:-2] + text = text[:len(text)-self.index] + '0' * self.index + if self.increase: + freq = int(text) + 10 ** self.index + else: + freq = int(text) - 10 ** self.index + if freq <= 0 and self.index > 0: + freq = 10 ** (self.index - 1) + #print ('X', x, 'N', n, text, 'freq', freq) + application.ChangeRxTxFrequency(None, freq) + def OnTimer(self, event): + if self.repeat_time == 300: # after first push, turn on repeats + self.repeat_time = 150 + elif self.repeat_time > 20: + self.repeat_time -= 5 + self.ChangeFreq() + self.timer.Start(milliseconds=self.repeat_time, oneShot=True) + def OnWheel(self, event): + index = self.GetIndex(event) + if index is not None: + self.index = index + if event.GetWheelRotation() > 0: + self.increase = True + else: + self.increase = False + self.ChangeFreq() + +class SliderBoxH: + """A horizontal control with a slider and text with a value. The text must have a %d or %f if display is True.""" + def __init__(self, parent, text, init, themin, themax, handler, display, pos, width, scale=1): + self.text = text + self.themin = themin + self.themax = themax + self.handler = handler + self.display = display + self.scale = scale + self.idName = text + if display: # Display the slider value + t1 = self.text % (themin * scale) + t2 = self.text % (themax * scale) + if len(t1) > len(t2): # set text size to the largest + t = t1 + else: + t = t2 + else: + t = self.text + if pos is None: + self.text_ctrl = wx.StaticText(parent, -1, t, style=wx.ST_NO_AUTORESIZE) + w2, h2 = self.text_ctrl.GetSize() + self.text_ctrl.SetSizeHints(w2, -1, w2) + self.slider = wx.Slider(parent, -1, init, themin, themax) + else: # Thanks to Stephen Hurd + self.text_ctrl = wx.StaticText(parent, -1, t, pos=pos) + w2, h2 = self.text_ctrl.GetSize() + self.slider = wx.Slider(parent, -1, init, themin, themax) + w3, h3 = self.slider.GetSize() + p2 = pos[1] + if h3 > h2: + p2 -= (h3 - h2) / 2 + else: + p2 += (h2 - h3) / 2 + self.slider.SetSize((width - w2, h3)) + self.slider.SetPosition((pos[0] + w2, p2)) + self.slider.Bind(wx.EVT_SCROLL, self.OnScroll) + self.text_ctrl.SetForegroundColour(parent.GetForegroundColour()) + self.OnScroll() + def OnScroll(self, event=None): + if event: + event.Skip() + if self.handler: + if application.remote_control_head: + application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetValue()}\n') + self.handler(event) + if self.display: + t = self.text % (self.slider.GetValue() * self.scale) + else: + t = self.text + self.text_ctrl.SetLabel(t) + def GetValue(self): + return self.slider.GetValue() + def SetValue(self, value): + # Set slider visual position; does not call handler + self.slider.SetValue(value) + self.OnScroll() + def GetDecValue(self): # Return the value 0.0 to 1.0 + return float(self.slider.GetValue() - self.themin) / (self.themax - self.themin) + def SetDecValue(self, value, do_cmd=True): # Set the value with a decimal 0.0 to 1.0 + if value < 0.0: + value = 0.0 + elif value > 1.0: + value = 1.0 + value = value * (self.themax - self.themin) + self.themin + value = int(value + 0.5) + self.SetValue(value) + if do_cmd: + if application.remote_control_head: + application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetValue()}\n') + self.handler(None) + def SetFocus(self): + self.slider.SetFocus() + +class SliderBoxHH(SliderBoxH, wx.BoxSizer): + """A horizontal control with a slider and text with a value. The text must have a %d if display is True.""" + def __init__(self, parent, text, init, themin, themax, handler, display, scale=1): + wx.BoxSizer.__init__(self, wx.HORIZONTAL) + SliderBoxH.__init__(self, parent, text, init, themin, themax, handler, display, None, None, scale) + #font = wx.Font(10, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + #self.text_ctrl.SetFont(font) + self.Add(self.text_ctrl, 0, wx.ALIGN_CENTER) + self.Add(self.slider, 1, wx.ALIGN_CENTER) + +class SliderBoxV(wx.BoxSizer): + """A vertical box containing a slider and a text heading""" + # Note: A vertical wx slider has the max value at the bottom. This is + # reversed for this control. + def __init__(self, parent, text, init, themax, handler, display=False, themin=0): + wx.BoxSizer.__init__(self, wx.VERTICAL) + self.slider = wx.Slider(parent, -1, init, themin, themax, style=wx.SL_VERTICAL|wx.SL_INVERSE) + self.slider.Bind(wx.EVT_SCROLL, self.OnScroll) + self.handler = handler + self.idName = text + sw, sh = self.slider.GetSize() + self.text = text + self.themin = themin + self.themax = themax + if display: # Display the slider value when it is thumb'd + self.text_ctrl = wx.StaticText(parent, -1, str(themax)) + self.text_ctrl.SetFont(button_font) + charw = self.text_ctrl.GetCharWidth() + w1, self.text_height = self.text_ctrl.GetSize() # Measure size with max number + w1 += charw + self.text_ctrl.SetLabel(str(themin)) + w3, h3 = self.text_ctrl.GetSize() # Measure size with min number + w3 += charw + self.text_ctrl.SetLabel(text) + w2, h2 = self.text_ctrl.GetSize() # Measure size with text + w2 += charw + self.width = max(w1, w2, w3, sw) + self.text_ctrl.SetSizeHints(self.width, -1, self.width) + self.slider.SetSizeHints(self.width, -1, self.width) + self.slider.Bind(wx.EVT_SCROLL_THUMBTRACK, self.Change) + self.slider.Bind(wx.EVT_SCROLL_THUMBRELEASE, self.ChangeDone) + else: + self.text_ctrl = wx.StaticText(parent, -1, text) + self.text_ctrl.SetFont(button_font) + charw = self.text_ctrl.GetCharWidth() + w2, self.text_height = self.text_ctrl.GetSize() # Measure size with text + w2 += charw + self.width = max(w2, sw) + self.text_ctrl.SetSizeHints(self.width, -1, self.width) + self.slider.SetSizeHints(self.width, -1, self.width) + self.text_ctrl.SetForegroundColour(parent.GetForegroundColour()) + self.Add(self.text_ctrl, 0, 0) + self.Add(self.slider, 1, 0) + def OnScroll(self, event): + if application.remote_control_head: + application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetValue()}\n') + self.handler(event) + def Change(self, event): + event.Skip() + self.text_ctrl.SetLabel(str(self.slider.GetValue())) + def ChangeDone(self, event): + event.Skip() + self.text_ctrl.SetLabel(self.text) + def GetValue(self): + return self.slider.GetValue() + def SetValue(self, value): + # Set slider visual position; does not call handler + self.slider.SetValue(value) + def GetDecValue(self): # Return the value 0.0 to 1.0 + return float(self.slider.GetValue() - self.themin) / (self.themax - self.themin) + def SetDecValue(self, value, do_cmd=True): # Set the value with a decimal 0.0 to 1.0 + if value < 0.0: + value = 0.0 + elif value > 1.0: + value = 1.0 + value = value * (self.themax - self.themin) + self.themin + value = int(value + 0.5) + self.SetValue(value) + if do_cmd: + self.OnScroll(None) + +class QuiskText1(wx.lib.stattext.GenStaticText): + # Self-drawn text for QuiskText. + def __init__(self, parent, size_text, height, style=0, fixed=False): + wx.lib.stattext.GenStaticText.__init__(self, parent, -1, '', + pos = wx.DefaultPosition, size = wx.DefaultSize, + style = wx.ST_NO_AUTORESIZE|style, + name = "QuiskText1") + self.fixed = fixed + self.size_text = size_text + self.pen = wx.Pen(conf.color_btn, 2) + self.brush = wx.Brush(conf.color_freq) + self.SetForegroundColour(conf.color_freq_txt) + self.SetSizeHints(1, height, 9999, height) + def _MeasureFont(self, dc, width, height): + # Set decreasing point size until size_text fits in the space available + for points in range(20, 6, -1): + if self.fixed: + font = wx.Font(points, wx.FONTFAMILY_MODERN, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + else: + font = wx.Font(points, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + dc.SetFont(font) + w, h = dc.GetTextExtent(self.size_text) + if w < width and h < height: + break + self.size_text = '' + self.SetFont(font) + def OnPaint(self, event): + dc = wx.PaintDC(self) + width, height = self.GetClientSize().Get() + if not width or not height: + return + dc.SetPen(self.pen) + dc.SetBrush(self.brush) + dc.DrawRectangle(1, 1, width-1, height-1) + label = self.GetLabel() + if not label: + return + if self.size_text: + self._MeasureFont(dc, width-2, height-2) + else: + dc.SetFont(self.GetFont()) + if self.IsEnabled(): + dc.SetTextForeground(self.GetForegroundColour()) + else: + dc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT)) + style = self.GetWindowStyleFlag() + w, h = dc.GetTextExtent(label) + y = (height - h) // 2 + if y < 0: + y = 0 + if style & wx.ALIGN_RIGHT: + x = width - w - 4 + elif style & wx.ALIGN_CENTER: + x = (width - w - 1)//2 + else: + x = 3 + dc.DrawText(label, x, y) + +class QuiskText(wx.BoxSizer): + # A one-line text display left/right/center justified and vertically centered. + # The height of the control is fixed as "height". The width is expanded. + # The font is chosen so size_text fits in the client area. + def __init__(self, parent, size_text, height, style=0, fixed=False): + wx.BoxSizer.__init__(self, wx.HORIZONTAL) + self.TextCtrl = QuiskText1(parent, size_text, height, style, fixed) + self.Add(self.TextCtrl, 1, flag=wx.ALIGN_CENTER_VERTICAL) + def SetLabel(self, label): + self.TextCtrl.SetLabel(label) + def GetLabel(self): + return self.TextCtrl.GetLabel() + +# Start of our button classes. They are compatible with wxPython GenButton +# buttons. Use the usual methods for access: +# GetLabel(self), SetLabel(self, label): Get and set the label +# Enable(self, flag), Disable(self), IsEnabled(self): Enable / Disable +# GetValue(self), SetValue(self, value): Get / Set check button state True / False +# SetIndex(self, index): For cycle buttons, set the label from its index + +class QuiskButtons: + """Base class for special buttons.""" + def __init__(self, idName): + self.idName = idName # idName can be '' + self.up_brush = wx.Brush(conf.color_btn) + r, g, b = self.up_brush.GetColour().Get(False) + r, g, b = min(255,r+32), min(255,g+32), min(255,b+32) + self.down_brush = wx.Brush(wx.Colour(r, g, b)) + self.color_disable = conf.color_disable + def InitButtons(self, text, text_color=None): + if text_color: + self.text_color = text_color + else: + self.text_color = conf.color_enable + self.SetBezelWidth(button_bezel) + self.SetBackgroundColour(conf.color_btn) + self.SetUseFocusIndicator(False) + self.decoration = None + self.char_shortcut = '' + self.SetFont(button_font) + if text: + w, h = self.GetTextExtent(text) + else: + w, h = self.GetTextExtent("OK") + self.Disable() # create a size for null text, but Disable() + w += button_bezel * 2 + self.GetCharWidth() + h = h * 12 // 10 + h += button_bezel * 2 + self.SetSizeHints(w, h, 999, h, 1, 1) + def DrawLabel(self, dc, width, height, dx=0, dy=0): # Override to change Disable text color + if self.up: # Clear the background here + dc.SetBrush(self.up_brush) + else: + dc.SetBrush(self.down_brush) + dc.SetPen(wx.TRANSPARENT_PEN) + bw = self.bezelWidth + dc.DrawRectangle(bw, bw, width - bw * 2, height - bw * 2) + dc.SetFont(self.GetFont()) + label = self.GetLabel() + tw, th = dc.GetTextExtent(label) + self.label_width = tw + dx = dy = self.labelDelta + slabel = re.split('('+u'\u25CF'+')', label) # unicode symbol for record: a filled dot + for part in slabel: # This code makes the symbol red. Thanks to Christof, DJ4CM. + if self.IsEnabled(): + if part == u'\u25CF': + dc.SetTextForeground('red') + else: + dc.SetTextForeground(self.text_color) + else: + dc.SetTextForeground(self.color_disable) + if self.char_shortcut: + scut = part.split(self.char_shortcut, 1) + if len(scut) == 2: # The shortcut character is present in the string + dc.DrawText(scut[0], (width-tw)//2+dx, (height-th)//2+dy) + dx += dc.GetTextExtent(scut[0])[0] + dc.SetFont(button_uline_font) + dc.DrawText(self.char_shortcut, (width-tw)//2+dx, (height-th)//2+dy) + dx += dc.GetTextExtent(self.char_shortcut)[0] + dc.SetFont(self.GetFont()) + dc.DrawText(scut[1], (width-tw)//2+dx, (height-th)//2+dy) + dx += dc.GetTextExtent(scut[1])[0] + else: + dc.DrawText(part, (width-tw)//2+dx, (height-th)//2+dy) + else: + dc.DrawText(part, (width-tw)//2+dx, (height-th)//2+dy) + dx += dc.GetTextExtent(part)[0] + if self.decoration and conf.decorate_buttons: + wd, ht = dc.GetTextExtent(self.decoration) + dc.DrawText(self.decoration, width - wd * 15 // 10, (height - ht) // 2) + def OnKeyDown(self, event): + pass + def OnKeyUp(self, event): + pass + def DrawGlyphCycle(self, dc, width, height): # Add a cycle indicator to the label + if not conf.decorate_buttons: + return + uch = conf.btn_text_cycle + wd, ht = dc.GetTextExtent(uch) + if wd * 2 + self.label_width > width: # not enough space + uch = conf.btn_text_cycle_small + wd, ht = dc.GetTextExtent(uch) + dc.DrawText(uch, width - wd, (height - ht) // 2) + else: + dc.DrawText(uch, width - wd * 15 // 10, (height - ht) // 2) + def SetColorGray(self): + self.SetBackgroundColour(wx.Colour(220, 220, 220)) # This sets the bezel colors + self.SetBezelWidth(2) + self.text_color = 'black' + self.color_disable = 'white' + self.up_brush = wx.Brush(wx.Colour(220, 220, 220)) + self.down_brush = wx.Brush(wx.Colour(240, 240, 240)) + +class QuiskBitmapButton(wx.lib.buttons.GenBitmapButton): + def __init__(self, parent, command, bitmap, use_right=False): + self.idName = "Bitmap" + self.command = command + self.bitmap = bitmap + wx.lib.buttons.GenBitmapButton.__init__(self, parent, -1, bitmap) + self.SetFont(button_font) + self.SetBezelWidth(button_bezel) + self.SetBackgroundColour(conf.color_btn) + self.SetUseFocusIndicator(False) + self.Bind(wx.EVT_BUTTON, self.OnButton) + self.direction = 1 + if use_right: + self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) + self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) + def DoGetBestSize(self): + return self.bitmap.GetWidth() + button_bezel * 2, self.bitmap.GetHeight() + button_bezel * 2 + def OnButton(self, event): + if self.command: + self.command(event) + def OnRightDown(self, event): + if self.GetBitmapLabel() == _bitmap_cyclepop: + self.OnLeftDown(event) + def OnRightUp(self, event): + if self.GetBitmapLabel() == _bitmap_cyclepop: + self.direction = -1 + self.OnLeftUp(event) + self.direction = 1 + +class QuiskPushbutton(QuiskButtons, wx.lib.buttons.GenButton): + """A plain push button widget.""" + def __init__(self, parent, command, text, use_right=False, text_color=None, style=0): + QuiskButtons.__init__(self, text) + wx.lib.buttons.GenButton.__init__(self, parent, -1, text, style=style) + self.command = command + self.Bind(wx.EVT_BUTTON, self.OnButton) + self.InitButtons(text, text_color) + self.direction = 1 + if use_right: + self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) + self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) + def OnButton(self, event): + if self.command: + if application.remote_control_head: + application.Hardware.RemoteCtlSend(self.idName + ';1\n') + self.command(event) + def OnRightDown(self, event): + self.direction = -1 + self.OnLeftDown(event) + def OnRightUp(self, event): + self.OnLeftUp(event) + self.direction = 1 + def SetIndex(self, index): + if self.command and index: + if application.remote_control_head: + application.Hardware.RemoteCtlSend(self.idName + ';1\n') + event = wx.PyEvent() + event.SetEventObject(self) + self.command(event) + + +class QuiskRepeatbutton(QuiskButtons, wx.lib.buttons.GenButton): + """A push button that repeats when held down.""" + def __init__(self, parent, command, text, up_command=None, use_right=False): + QuiskButtons.__init__(self, text) + wx.lib.buttons.GenButton.__init__(self, parent, -1, text) + self.command = command + self.up_command = up_command + self.timer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.OnTimer) + self.Bind(wx.EVT_BUTTON, self.OnButton) + self.InitButtons(text) + self.repeat_state = 0 # repeater button inactive + self.direction = 1 + if use_right: + self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) + self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) + def SendCommand(self, command): + if command: + event = wx.PyEvent() + event.SetEventObject(self) + command(event) + def OnLeftDown(self, event): + if self.IsEnabled(): + self.shift = event.ShiftDown() + self.control = event.ControlDown() + self.SendCommand(self.command) + self.repeat_state = 1 # first button push + self.timer.Start(milliseconds=300, oneShot=True) + wx.lib.buttons.GenButton.OnLeftDown(self, event) + def OnLeftUp(self, event): + if self.IsEnabled(): + self.repeat_state = 0 + self.timer.Stop() + self.SendCommand(self.up_command) + wx.lib.buttons.GenButton.OnLeftUp(self, event) + def OnRightDown(self, event): + if self.IsEnabled(): + self.shift = event.ShiftDown() + self.control = event.ControlDown() + self.direction = -1 + self.OnLeftDown(event) + def OnRightUp(self, event): + if self.IsEnabled(): + self.OnLeftUp(event) + self.direction = 1 + def OnTimer(self, event): + if self.repeat_state == 1: # after first push, turn on repeats + self.timer.Start(milliseconds=150, oneShot=False) + self.repeat_state = 2 + if self.repeat_state: # send commands until button is released + self.SendCommand(self.command) + def OnButton(self, event): + pass # button command not used + def Shortcut(self, event, button_name=None): + if not self.IsEnabled(): + return + if button_name == "_end_": + self.repeat_state = 0 + self.timer.Stop() + self.SendCommand(self.up_command) + else: + self.shift = False + self.control = False + self.SendCommand(self.command) + self.repeat_state = 1 # first button push + self.timer.Start(milliseconds=300, oneShot=True) + +class QuiskCheckbutton(QuiskButtons, wx.lib.buttons.GenToggleButton): + """A button that pops up and down, and changes color with each push.""" + # Check button; get the checked state with self.GetValue() + def __init__(self, parent, command, text, color=None, use_right=False): + QuiskButtons.__init__(self, text) + wx.lib.buttons.GenToggleButton.__init__(self, parent, -1, text) + self.InitButtons(text) + self.Bind(wx.EVT_BUTTON, self.OnButton) + self.button_down = 0 # used for radio buttons + self.command = command + if color is None: + self.down_brush = wx.Brush(conf.color_check_btn) + else: + self.down_brush = wx.Brush(color) + self.direction = 1 + if use_right: + self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) + self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) + def SetValue(self, value, do_cmd=False): + wx.lib.buttons.GenToggleButton.SetValue(self, value) + self.button_down = value + if do_cmd and self.command: + if application.remote_control_head: + application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetIndex()}\n') + event = wx.PyEvent() + event.SetEventObject(self) + self.command(event) + def SetIndex(self, index, do_cmd=False): + self.SetValue(bool(index), do_cmd) + def GetIndex(self): + if self.GetValue(): + return 1 + else: + return 0 + def OnButton(self, event): + if self.command: + if application.remote_control_head: + application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetIndex()}\n') + self.command(event) + def OnRightDown(self, event): + self.direction = -1 + self.OnLeftDown(event) + def OnRightUp(self, event): + self.OnLeftUp(event) + self.direction = 1 + def Shortcut(self, event, button_name=None): + if self.IsEnabled(): + self.SetValue(not self.GetValue(), True) + +class QuiskBitField(wx.Window): + """A control used to set/unset bits.""" + def __init__(self, parent, numbits, value, height, command): + self.numbits = numbits + self.value = value + self.height = height + self.command = command + self.backgroundBrush = wx.Brush('white') + self.pen = wx.Pen('light gray', 1) + self.font = parent.GetFont() + self.charx, self.chary = parent.GetTextExtent('1') + self.linex = [] # x pixel of vertical lines + self.bitx = [] # x pixel of character for bits + space = self.space = max(2, self.charx * 2 // 10) + width = 0 + for i in range(numbits - 1): + width += space + self.charx + space + self.linex.append(width) + width = space + for i in range(numbits): + self.bitx.append(width) + width += self.charx + space * 2 + wx.Window.__init__(self, parent, size=(width + 4, height), style=wx.BORDER_SUNKEN) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + def OnPaint(self, event): + dc = wx.PaintDC(self) + dc.SetBackground(self.backgroundBrush) + dc.Clear() + dc.SetFont(self.font) + dc.SetPen(self.pen) + for x in self.linex: + dc.DrawLine(x, 0, x, self.height) + for i in range(self.numbits): + power = self.numbits - i - 1 + x = self.bitx[i] + if self.value & (1 << power): + dc.DrawText('1', x, 0) + def OnLeftDown(self, event): + mouse_x, mouse_y = event.GetPosition().Get() + for index in range(len(self.linex)): + if mouse_x < self.linex[index]: + break + else: + index = self.numbits - 1 + power = self.numbits - index - 1 + mask = 1 << power + if self.value & mask: + self.value &= ~ mask + else: + self.value |= mask + self.Refresh() + if self.command: + self.command(self) + +class QFilterButtonWindow(wx.Frame): + """Create a window with controls for the button""" + def __init__(self, wrap, value): + self.wrap = wrap + l = self.valuelist = [] + bw = 10 + incr = 10 + for i in range(0, 101): + l.append(bw) + bw += incr + if bw == 100: + incr = 20 + elif bw == 500: + incr = 50 + elif bw == 1000: + incr = 100 + elif bw == 5000: + incr = 500 + elif bw == 10000: + incr = 1000 + x, y = wrap.GetPosition().Get() + x, y = wrap.GetParent().ClientToScreen(wx.Point(x, y)) + w, h = wrap.GetSize() + height = h * 12 + size = (w, height) + if sys.platform == 'win32': + pos = (x, y - height - h) + t = 'Filter' + else: + pos = (x, y - height - h) + t = '' + wx.Frame.__init__(self, wrap.GetParent(), -1, t, pos, size, + wx.FRAME_TOOL_WINDOW|wx.FRAME_FLOAT_ON_PARENT|wx.CLOSE_BOX|wx.CAPTION|wx.SYSTEM_MENU) + self.SetSizeHints(w, height, w, height) + hbox = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(hbox) + self.SetBackgroundColour(conf.color_freq) + self.Bind(wx.EVT_CLOSE, self.OnClose) + try: + index = self.valuelist.index(value) + except ValueError: + index = 0 + self.wrap.button.slider_value = self.valuelist[0] + self.slider = wx.Slider(self, -1, index, 0, 100, style=wx.SL_VERTICAL|wx.SL_INVERSE) + hbox.Add(self.slider, flag=wx.CENTER, proportion=1) + self.slider.Bind(wx.EVT_SCROLL, self.OnSlider) + self.SetTitle("%d" % self.valuelist[index]) + self.Show() + self.slider.SetFocus() + self.Fit() + def OnSlider(self, event): + index = self.slider.GetValue() + value = self.valuelist[index] + self.SetTitle("%d" % value) + self.wrap.ChangeSlider(value) + def OnClose(self, event): + self.wrap.adjust = None + self.Destroy() + +class QSliderButtonWindow(wx.Frame): + """Create a window with controls for the button""" + def __init__(self, button, value): + self.button = button + x, y = button.GetPosition().Get() + x, y = button.GetParent().ClientToScreen(wx.Point(x, y)) + w, h = button.GetSize() + height = h * 12 + size = (w, height) + if sys.platform == 'win32': + pos = (x, y - height - h) + else: + pos = (x, y - height - h) + wx.Frame.__init__(self, button.GetParent(), -1, '', pos, size, + wx.FRAME_TOOL_WINDOW|wx.FRAME_FLOAT_ON_PARENT|wx.CLOSE_BOX|wx.CAPTION|wx.SYSTEM_MENU) + self.SetSizeHints(w, height, w, height) + hbox = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(hbox) + self.SetBackgroundColour(conf.color_freq) + self.Bind(wx.EVT_CLOSE, self.OnClose) + self.slider = wx.Slider(self, -1, value, + self.button.slider_min, self.button.slider_max, style=wx.SL_VERTICAL|wx.SL_INVERSE) + hbox.Add(self.slider, flag=wx.CENTER, proportion=1) + self.slider.Bind(wx.EVT_SCROLL, self.OnSlider) + if self.button.display: + value = float(value) / self.button.slider_max + self.SetTitle("%6.3f" % value) + self.Show() + self.slider.SetFocus() + self.Fit() + def OnSlider(self, event): + value = self.slider.GetValue() + if self.button.display: + v = float(value) / self.button.slider_max + self.SetTitle("%6.3f" % v) + self.button.ChangeSlider(value) + def OnClose(self, event): + self.button.adjust = None + self.Destroy() + +# Dual slider widget for bias +class QDualSliderButtonWindow(wx.Frame): # Thanks to Steve, KF7O + """Create a window with controls for the button""" + def __init__(self, button): + self.button = button + x, y = button.GetPosition().Get() + x, y = button.GetParent().ClientToScreen(wx.Point(x, y)) + w, h = button.GetSize() + w = w * 12 // 10 + height = h * 10 + size = (w, height) + if sys.platform == 'win32': + pos = (x, y - height) + else: + pos = (x, y - height - h) + wx.Frame.__init__(self, button.GetParent(), -1, '', pos, size, + wx.FRAME_TOOL_WINDOW|wx.FRAME_FLOAT_ON_PARENT|wx.CLOSE_BOX|wx.CAPTION|wx.SYSTEM_MENU) + self.SetBackgroundColour(conf.color_freq) + self.Bind(wx.EVT_CLOSE, self.OnClose) + panel = wx.Panel(self, -1) + panel.SetBackgroundColour(conf.color_freq) + hbox = wx.BoxSizer(wx.HORIZONTAL) + self.lslider = wx.Slider(panel, -1, self.button.lslider_value, + self.button.slider_min, self.button.slider_max, + (0, 0), (w//2, height), wx.SL_VERTICAL|wx.SL_INVERSE) + self.lslider.Bind(wx.EVT_SCROLL, self.OnSlider) + hbox.Add(self.lslider, flag=wx.LEFT) + self.rslider = wx.Slider(panel, -1, self.button.rslider_value, + self.button.slider_min, self.button.slider_max, + (0, 0), (w//2, height), wx.SL_VERTICAL|wx.SL_INVERSE) + self.rslider.Bind(wx.EVT_SCROLL, self.OnSlider) + hbox.Add(self.rslider, flag=wx.RIGHT) + panel.SetSizer(hbox) + if self.button.display: + self.SetTitle("%3d %3d" % (self.button.lslider_value,self.button.rslider_value)) + self.Show() + self.lslider.SetFocus() + def OnSlider(self, event): + lvalue = self.lslider.GetValue() + rvalue = self.rslider.GetValue() + self.button.ChangeSlider(lvalue,rvalue) + if self.button.display: + self.SetTitle("%3d %3d" % (self.button.lslider_value,self.button.rslider_value)) + def OnClose(self, event): + self.button.adjust = None + self.Destroy() + +class WrapControl(wx.BoxSizer): + def __init__(self): + wx.BoxSizer.__init__(self, wx.HORIZONTAL) + def Enable(self, value=True): + self.button.Enable(value) + def SetLabel(self, text=None): + if text is not None: + self.button.SetLabel(text) + def GetParent(self): + return self.button.GetParent() + def GetValue(self): + return self.button.GetValue() + def SetValue(self, value, do_cmd=False): + self.button.SetValue(value, do_cmd) + def SetIndex(self, index, do_cmd=False): + self.button.SetIndex(index, do_cmd) + def GetLabel(self): + return self.button.GetLabel() + def __getattr__(self, name): + return getattr(self.button, name) + +class WrapPushButton(WrapControl): + def __init__(self, button, control): + self.button = button + WrapControl.__init__(self) + self.Add(button, 1, flag=wx.ALIGN_CENTER_VERTICAL) + b = QuiskPushbutton(button.GetParent(), control, conf.btn_text_switch) + self.Add(b, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=2) + +class WrapIndicator(WrapControl): + def __init__(self, button, text, text_color, on_color): + self.button = button + self.text = text + self.on_brush = wx.Brush(on_color) + WrapControl.__init__(self) + self.Add(button, 1, flag=wx.ALIGN_CENTER_VERTICAL) + self.light = QuiskPushbutton(button.GetParent(), None, "", text_color=text_color) + self.Add(self.light, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=2) + def TurnOn(self, on): + if on: + self.light.SetLabel(self.text) + self.light.up_brush = self.on_brush + else: + self.light.SetLabel("") + self.light.up_brush = self.button.up_brush + self.light.Refresh() + +class WrapMenu(WrapControl): + def __init__(self, button, menu, on_open=None): + self.button = button + self.menu = menu + self.on_open = on_open + WrapControl.__init__(self) + self.Add(button, 1, flag=wx.ALIGN_CENTER_VERTICAL) + b = QuiskBitmapButton(button.GetParent(), self.OnPopButton, _bitmap_menupop) + self.Add(b, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=2) + self.pop_button = b + def OnPopButton(self, event): + if self.on_open: + self.on_open(self.menu) + pos = (5, 5) + self.pop_button.PopupMenu(self.menu, pos) + +class WrapSlider(WrapControl): + def __init__(self, button, command, slider_value=0, slider_min=0, slider_max=1000, display=False, wintype=''): + self.adjust = None + self.dual = False # dual means separate slider values for on and off + self.button = button + self.main_command = button.command + button.command = self.OnMainButton + self.command = command + button.slider_value = slider_value # value for not dual + button.slider_value_off = slider_value # value for dual and button up + button.slider_value_on = slider_value # value for dual and button down + self.slider_min = slider_min + self.slider_max = slider_max + self.display = display # Display the value at the top + self.wintype = wintype + WrapControl.__init__(self) + self.Add(button, 1, flag=wx.ALIGN_CENTER_VERTICAL) + b = QuiskBitmapButton(button.GetParent(), self.OnPopButton, _bitmap_sliderpop) + self.Add(b, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=2) + def SetDual(self, dual): # dual means separate slider values for on and off + self.dual = dual + if self.adjust: + self.adjust.Destroy() + self.adjust = None + def DeleteSliderWindow(self): + if self.adjust: + self.adjust.Destroy() + self.adjust = None + def OnPopButton(self, event): + if self.adjust: + self.adjust.Destroy() + self.adjust = None + else: + if not self.dual: + value = self.button.slider_value + elif self.button.GetValue(): + value = self.button.slider_value_on + else: + value = self.button.slider_value_off + if self.wintype == 'filter': + self.adjust = QFilterButtonWindow(self, value) + else: + self.adjust = QSliderButtonWindow(self, value) + def OnMainButton(self, event): + if self.adjust: + self.adjust.Destroy() + self.adjust = None + if self.main_command: + self.main_command(event) + def ChangeSlider(self, value): + if not self.dual: + self.button.slider_value = value + elif self.button.GetValue(): + self.button.slider_value_on = value + else: + self.button.slider_value_off = value + if self.wintype == 'filter': + self.button.SetLabel(str(value)) + self.button.Refresh() + if self.command: + if application.remote_control_head: + application.Hardware.RemoteCtlSend(f'{self.idName}Slider;{value}\n') + event = wx.PyEvent() + event.SetEventObject(self.button) + self.command(event) + def SetSlider(self, value=None, value_off=None, value_on=None): + if value is not None: + self.button.slider_value = value + if value_off is not None: + self.button.slider_value_off = value_off + if value_on is not None: + self.button.slider_value_on = value_on + +class WrapDualSlider(WrapControl): # Thanks to Steve, KF7O + def __init__(self, button, command, lslider_value=0, rslider_value=0, slider_min=0, slider_max=1000, display=0): + self.adjust = None + self.button = button + self.main_command = button.command + button.command = self.OnMainButton + self.command = command + button.lslider_value = lslider_value + button.rslider_value = rslider_value + self.slider_min = slider_min + self.slider_max = slider_max + self.display = display # Display the value at the top + WrapControl.__init__(self) + self.Add(button, 1, flag=wx.ALIGN_CENTER_VERTICAL) + ## This is a hack to get _bitmap_sliderpop + ## It would be better if _bitmap_sliderpop were not a global variable + ##but a first-class member in another module + _bitmap_sliderpop = MakeWidgetGlobals.__globals__['_bitmap_sliderpop'] + b = QuiskBitmapButton(button.GetParent(), self.OnPopButton, _bitmap_sliderpop) + self.Add(b, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=2) + def OnPopButton(self, event): + if self.adjust: + self.adjust.Destroy() + self.adjust = None + else: + self.adjust = QDualSliderButtonWindow(self) + def OnMainButton(self, event): + if self.adjust: + self.adjust.Destroy() + self.adjust = None + if self.main_command: + self.main_command(event) + def ChangeSlider(self, lvalue, rvalue): + self.button.lslider_value = lvalue + self.button.rslider_value = rvalue + if self.command: + event = wx.PyEvent() + event.SetEventObject(self.button) + self.command(event) + +class QuiskCycleCheckbutton(QuiskCheckbutton): + """A button that cycles through its labels with each push. + + The button is up for labels[0], down for all other labels. Change to the + next label for each push. If you call SetLabel(), the label must be in the list. + The self.index is the index of the current label. + """ + def __init__(self, parent, command, labels, color=None, is_radio=False): + self.labels = list(labels) # Be careful if you change this list + self.index = 0 # index of selected label 0, 1, ... + self.direction = 0 # 1 for up, -1 for down, 0 for no change to index + self.is_radio = is_radio # Is this a radio cycle button? + if color is None: + color = conf.color_cycle_btn + QuiskCheckbutton.__init__(self, parent, command, labels[0], color) + self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) + self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDclick) + def SetLabel(self, label, do_cmd=False): + self.index = self.labels.index(label) + QuiskCheckbutton.SetLabel(self, label) + QuiskCheckbutton.SetValue(self, self.index) + if do_cmd and self.command: + if application.remote_control_head: + application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetIndex()}\n') + event = wx.PyEvent() + event.SetEventObject(self) + self.command(event) + def SetIndex(self, index, do_cmd=False): + self.index = index + QuiskCheckbutton.SetLabel(self, self.labels[index]) + QuiskCheckbutton.SetValue(self, index) + if do_cmd and self.command: + if application.remote_control_head: + application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetIndex()}\n') + event = wx.PyEvent() + event.SetEventObject(self) + self.command(event) + def GetIndex(self): + return self.index + def OnButton(self, event): + if not self.is_radio or self.button_down: + self.direction = 1 + self.index += 1 + if self.index >= len(self.labels): + self.index = 0 + self.SetIndex(self.index) + else: + self.direction = 0 + if self.command: + if application.remote_control_head: + application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetIndex()}\n') + self.command(event) + def OnRightDown(self, event): # Move left in the list of labels + if not self.is_radio or self.GetValue(): + self.index -= 1 + if self.index < 0: + self.index = len(self.labels) - 1 + self.SetIndex(self.index) + self.direction = -1 + if self.command: + if application.remote_control_head: + application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetIndex()}\n') + self.command(event) + def OnLeftDclick(self, event): # Left double-click: Set index zero + if not self.is_radio or self.GetValue(): + self.index = 0 + self.SetIndex(self.index) + self.direction = 1 + if self.command: + if application.remote_control_head: + application.Hardware.RemoteCtlSend(f'{self.idName};{self.GetIndex()}\n') + self.command(event) + def DrawLabel(self, dc, width, height, dx=0, dy=0): + QuiskCheckbutton.DrawLabel(self, dc, width, height, dx, dy) + self.DrawGlyphCycle(dc, width, height) + def Shortcut(self, event, button_name=None): + if self.IsEnabled(): + if not self.is_radio or self.button_down: + self.direction = 1 + index = self.index + 1 + if index >= len(self.labels): + index = 0 + self.SetIndex(index, True) + else: + self.SetIndex(self.index, True) + +class RadioButtonGroup: + """This class encapsulates a group of radio buttons. This class is not a button! + + The "labels" is a list of labels for the toggle buttons. An item + of labels can be a list/tuple, and the corresponding button will + be a cycle button. + """ + def __init__(self, parent, command, labels, default, shortcuts=()): + self.command = command + self.buttons = [] # This list of buttons can also contain WrapControls + self.button = None + self.shortcuts = list(shortcuts[:]) + self.last_shortcut = 0 + i = 0 + for text in labels: + if isinstance(text, (list, tuple)): + b = QuiskCycleCheckbutton(parent, self.OnButton, text, is_radio=True) + if shortcuts: + b.char_shortcut = shortcuts[i] + for t in text: + if t == default and self.button is None: + b.SetLabel(t) + self.button = b + else: + b = QuiskCheckbutton(parent, self.OnButton, text) + if shortcuts: + b.char_shortcut = shortcuts[i] + if text == default and self.button is None: + b.SetValue(True) + self.button = b + self.buttons.append(b) + i += 1 + def ReplaceButton(self, index, button): # introduce a specialized button + b = self.buttons[index] + b.Destroy() + self.buttons[index] = button + if isinstance(button, WrapSlider): + button.main_command = self.OnButton + elif isinstance(button, WrapMenu): + button.button.command = self.OnButton + else: + button.command = self.OnButton + def SetLabel(self, label, do_cmd=False, direction=None): + if application.remote_control_head: + func = "%s.SetLabel" % self.idName + application.Hardware.RemoteCtlSend(f'JsonAppFunc;{json.dumps((func, label, do_cmd, direction))}\n') + self.button = None + for b in self.buttons: + if self.button is not None: + b.SetValue(False) + elif isinstance(b, QuiskCycleCheckbutton) or (isinstance(b, WrapControl) and isinstance(b.button, QuiskCycleCheckbutton)): + try: + index = b.labels.index(label) + except ValueError: + b.SetValue(False) + continue + else: + b.SetIndex(index) + self.button = b + b.SetValue(True) + if direction is not None: + if isinstance(b, WrapControl): + b.button.direction = direction + else: + b.direction = direction + elif b.GetLabel() == label: + b.SetValue(True) + self.button = b + else: + b.SetValue(False) + if do_cmd and self.command and self.button: + event = wx.PyEvent() + event.SetEventObject(self.button) + self.command(event) + def GetButtons(self): + return self.buttons + def OnButton(self, event): + win = event.GetEventObject() + for b in self.buttons: + if b is win or (isinstance(b, WrapControl) and b.button is win): + self.button = b + b.SetValue(True) + else: + b.SetValue(False) + if self.command: + self.command(event) + def GetLabel(self): + if not self.button: + return None + return self.button.GetLabel() + def GetSelectedButton(self): # return the selected button + return self.button + def GetIndex(self): # Careful. Some buttons are WrapControls. + if not self.button: + return None + return self.buttons.index(self.button) + def Shortcut(self, event): # Not used for Midi + # Multiple buttons can have the same shortcut, so move to the next one. + index = self.last_shortcut + 1 + length = len(self.shortcuts) + if index >= length: + index = 0 + for i in range(length): + shortcut = self.shortcuts[index] + if shortcut and application.QuiskGetKeyState(ord(shortcut)): + break + index += 1 + if index >= length: + index = 0 + else: + return + self.last_shortcut = index + button = self.buttons[index] + event = wx.PyEvent() + event.SetEventObject(button) + button.OnButton(event) + +class _PopWindow(wx.PopupWindow): + def __init__(self, parent, command, labels, default): + wx.PopupWindow.__init__(self, parent) + self.panel = wx.Panel(self) + self.panel.SetBackgroundColour(conf.color_popchoice) + self.RbGroup = RadioButtonGroup(self.panel, command, labels, default) + x = 5 + y = 5 + for b in self.RbGroup.buttons: + b.SetPosition((x, y)) + w, h = b.GetTextExtent(b.GetLabel()) + width = w + 2 + 2 * b.bezelWidth + 4 * int(b.useFocusInd) + height = h + 2 + 2 * b.bezelWidth + 4 * int(b.useFocusInd) + b.SetInitialSize((width, height)) + x += width + 5 + self.SetSize((x, height + 2 * y)) + self.panel.SetSize((x, height + 2 * y)) + +class RadioBtnPopup: + """This class contains a button that pops up a row of radio buttons""" + def __init__(self, parent, command, in_labels, default, idName): + self.parent = parent + self.pop_command = command + self.button_data = {} + labels = [] + for item in in_labels: + if isinstance(item, (list, tuple)): + labels.append(item[0]) + self.button_data[item[0]] = [_bitmap_cyclepop, 0, len(item)] # bitmap, index, max_index + else: + labels.append(item) + self.RbDialog = _PopWindow(parent, self.OnGroupButton, labels, default) + self.RbDialog.RbGroup.idName = idName + self.RbDialog.Hide() + self.pop_control = wx.BoxSizer(wx.HORIZONTAL) + self.first_button = QuiskPushbutton(parent, self.OnFirstButton, labels[0], text_color=conf.color_popchoice) + self.first_button.decoration = u'\u21D2' + self.first_button.idName = idName + self.second_button = QuiskBitmapButton(parent, self.OnSecondButton, _bitmap_menupop, use_right=True) + self.pop_control.Add(self.first_button, 1, flag=wx.ALIGN_CENTER_VERTICAL) + self.pop_control.Add(self.second_button, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=2) + self.pop_control.Show(self.second_button, False) + self.adjust = None + self.first_button.index = 0 + def GetPopControl(self): + return self.pop_control + def AddMenu(self, label, menu): + self.button_data[label] = (_bitmap_menupop, menu) + return self + def AddSlider(self, label, command, slider_value=0, slider_min=0, slider_max=1000, display=False, wintype=''): + self.button_data[label] = [_bitmap_sliderpop, command, slider_value, slider_min, slider_max, display, wintype] + def OnFirstButton(self, event): + if self.adjust: # Destroy any slider window + self.adjust.Destroy() + self.adjust = None + if self.RbDialog.IsShown(): + self.RbDialog.Hide() + return + dw, dh = self.RbDialog.GetSize().Get() + bw, bh = self.first_button.GetSize().Get() + bx, by = self.first_button.ClientToScreen(wx.Point(0, 0)) + self.RbDialog.Position(wx.Point(bx + bw * 8 // 10, by + bh // 2 - dh), wx.Size(1, 1)) + self.RbDialog.Show() + self.RbDialog.SetFocus() + def AddSecondButton(self, label): + data = self.button_data.get(label, None) + if data is None: + self.pop_control.Show(self.second_button, False) + else: + self.second_button.SetBitmapLabel(data[0]) + self.pop_control.Show(self.second_button, True) + if data[0] == _bitmap_cyclepop: + self.first_button.index = data[1] + self.pop_control.Layout() + def OnSecondButton(self, event): + label = self.first_button.GetLabel() + data = self.button_data.get(label, None) + if data is None: + pass + elif data[0] == _bitmap_menupop: + self.first_button.PopupMenu(data[1], (5, 5)) + elif data[0] == _bitmap_sliderpop: + if self.adjust: + self.adjust.Destroy() + self.adjust = None + else: + bitm, self.command, slider_value, self.slider_min, self.slider_max, self.display, self.wintype = data + self.adjust = QSliderButtonWindow(self, slider_value) + self.second_data = data + elif data[0] == _bitmap_cyclepop: + if self.second_button.direction >= 0: + data[1] += 1 + if data[1] >= data[2]: + data[1] = 0 + else: + data[1] -= 1 + if data[1] < 0: + data[1] = data[2] - 1 + self.first_button.index = data[1] + self.first_button.direction = self.second_button.direction + if self.pop_command: + event = wx.PyEvent() + event.SetEventObject(self.first_button) + self.pop_command(event) + self.first_button.direction = 1 + def OnGroupButton(self, event): + btn = event.GetEventObject() + label = btn.GetLabel() + self.first_button.SetLabel(label) + self.first_button.Refresh() + self.RbDialog.Hide() + self.AddSecondButton(label) + if self.pop_command: + event = wx.PyEvent() + event.SetEventObject(self.first_button) + self.pop_command(event) + def Enable(self, label, enable): + for b in self.RbDialog.RbGroup.buttons: + if b.GetLabel() == label: + b.Enable(enable) + break + def GetLabel(self): + return self.first_button.GetLabel() + def SetLabel(self, label, do_cmd=False, direction=None): + direc = self.first_button.direction + if direction is not None: + self.first_button.direction = direction + self.first_button.SetLabel(label) + self.AddSecondButton(label) + self.RbDialog.RbGroup.SetLabel(label, False) + if do_cmd and self.pop_command: + event = wx.PyEvent() + event.SetEventObject(self.first_button) + self.pop_command(event) + self.first_button.direction = direc + def GetButtons(self): + return self.RbDialog.RbGroup.GetButtons() + def Refresh(self): + pass + def ChangeSlider(self, slider_value): + self.second_data[2] = slider_value + command = self.second_data[1] + if command: + event = wx.PyEvent() + self.first_button.slider_value = slider_value + event.SetEventObject(self.first_button) + command(event) + def GetPositionTuple(self): + return self.first_button.GetPosition().Get() + def GetParent(self): + return self.parent + def GetSize(self): + return self.first_button.GetSize() + def Shortcut(self, event, button_name=None): + self.SetLabel(button_name, True) + def GetSelectedButton(self): # return the selected button + return self.RbDialog.RbGroup.GetSelectedButton() + +class FreqSetter(wx.TextCtrl): + def __init__(self, parent, x, y, label, fmin, fmax, freq, command): + self.pos = (x, y) + self.label = label + self.fmin = fmin + self.fmax = fmax + self.command = command + self.font = wx.Font(16, wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL, False, conf.quisk_typeface) + t = wx.StaticText(parent, -1, label, pos=(x, y)) + t.SetFont(self.font) + freq_w, freq_h = t.GetTextExtent(" 662 000 000") + tw, th = t.GetSize().Get() + x += tw + 20 + wx.TextCtrl.__init__(self, parent, size=(freq_w, freq_h), pos=(x, y), + style=wx.TE_RIGHT|wx.TE_PROCESS_ENTER) + self.SetFont(self.font) + self.Bind(wx.EVT_TEXT, self.OnText) + self.Bind(wx.EVT_TEXT_ENTER, self.OnEnter) + w, h = self.GetSize().Get() + x += w + 1 + self.butn = b = wx.SpinButton(parent, size=(freq_h, freq_h), pos=(x, y)) + w, h = b.GetSize().Get() + self.end_pos = (x + w, y + h) + b.Bind(wx.EVT_SPIN, self.OnSpin) # The spin button frequencies are in kHz + b.SetMin(fmin // 1000) + b.SetMax(fmax // 1000) + self.SetValue(freq) + def OnText(self, event): + self.SetBackgroundColour('pink') + def OnEnter(self, event): + text = wx.TextCtrl.GetValue(self) + text = text.replace(' ', '') + if '-' in text: + return + try: + if '.' in text: + freq = int(float(text) * 1000000 + 0.5) + else: + freq = int(text) + except: + return + self.SetValue(freq) + self.command(self) + def OnSpin(self, event): + freq = self.butn.GetValue() * 1000 + self.SetValue(freq) + self.command(self) + def SetValue(self, freq): + if freq < self.fmin: + freq = self.fmin + elif freq > self.fmax: + freq = self.fmax + self.butn.SetValue(freq // 1000) + txt = FreqFormatter(freq) + wx.TextCtrl.SetValue(self, txt) + self.SetBackgroundColour(conf.color_entry) + def GetValue(self): + value = wx.TextCtrl.GetValue(self) + value = value.replace(' ', '') + try: + value = int(value) + except: + value = 7000 + return value + +class QuiskMenu(wx.Menu): + def __init__(self, menu_name): + wx.Menu.__init__(self) + self.menu_name = menu_name + self.id2data = {} + self.item_text2id = {} + def InitData(self, item, handler): + self.Bind(wx.EVT_MENU, self.Handler, item) + item_text = item.GetItemLabelText() + self.id2data[item.GetId()] = (item_text, handler) + self.item_text2id[item_text] = item.GetId() + def Append(self, item_text, handler): + item = wx.Menu.Append(self, -1, item_text) + self.InitData(item, handler) + return item + def AppendCheckItem(self, item_text, handler, is_checked=0): + item = wx.Menu.AppendCheckItem(self, -1, item_text) + item.Check(is_checked) + self.InitData(item, handler) + return item + def AppendRadioItem(self, item_text, handler, is_checked=0): + item = wx.Menu.AppendRadioItem(self, -1, item_text) + item.Check(is_checked) + self.InitData(item, handler) + return item + def IsItemChecked(self, item_text): + nid = self.item_text2id[item_text] + menu_item = self.FindItemById(nid) + return menu_item.IsChecked() + def Handler(self, event=None, nid=0): + if event: + nid = event.GetId() + item_text, handler = self.id2data[nid] + if application.remote_control_head: + menu_item = self.FindItemById(nid) + if menu_item.IsCheckable(): + checked = menu_item.IsChecked() + else: + checked = 0 + application.Hardware.RemoteCtlSend('MENU;%s;%s;%d\n' % (self.menu_name, item_text, int(checked))) + handler(event) diff --git a/sdrmicronpkg/__init__.py b/sdrmicronpkg/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/sdrmicronpkg/__init__.py @@ -0,0 +1 @@ +# diff --git a/sdrmicronpkg/quisk_hardware.py b/sdrmicronpkg/quisk_hardware.py new file mode 100644 index 0000000..7c48db7 --- /dev/null +++ b/sdrmicronpkg/quisk_hardware.py @@ -0,0 +1,266 @@ +# -*- coding: cp1251 -*- +# +# It provides support for the SDR Micron + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import sys, wx, traceback + +import ftd2xx as d2xx +import time + +from quisk_hardware_model import Hardware as BaseHardware + +DEBUG = 0 + +# https://github.com/Dfinitski/SDR-Micron +# +# RX control, to device, 32 bytes total +# Preamble + 'RX0' + enable + rate + 4 bytes frequency + attenuation + 14 binary zeroes +# +# where: +# Preamble is 7*0x55, 0xd5 +# bytes: +# enable - binary 0 or 1, for enable receiver +# rate: +# binary +# 0 for 48 kHz +# 1 for 96 kHz +# 2 for 192 kHz +# 3 for 240 kHz +# 4 for 384 kHz +# 5 for 480 kHz +# 6 for 640 kHz +# 7 for 768 kHz +# 8 for 960 kHz +# 9 for 1536 kHz +# 10 for 1920 kHz +# +# frequency - 32 bits of tuning frequency, MSB is first +# attenuation - binary 0, 10, 20, 30 for needed attenuation +# +# RX data, to PC, 508 bytes total +# Preamble + 'RX0' + FW1 + FW2 + CLIP + 2 zeroes + 492 bytes IQ data +# +# Where: +# FW1 and FW2 - char digits firmware version number +# CLIP - ADC overflow indicator, 0 or 1 binary +# IQ data for 0 - 7 rate: +# 82 IQ pairs formatted as "I2 I1 I0 Q2 Q1 Q0.... ", MSB is first, 24 bits per sample +# IQ data for 8 - 10 rate: +# 123 IQ pairs formatted as "I1 I0 Q1 Q0..... ", MSB is first, 16 bits per sample +# +# Band Scope control, to device, 32 bytes total +# Preamble + 'BS0' + enable + period + 19 binary zeroes +# +# Where period is the full frame period in ms, from 50 to 255ms, 100ms is recommended +# for 10Hz refresh rate window. +# +# Band Scope data, to PC, 16384 16bit samples, 32768 bytes by 492 in each packet +# Preamble + 'BS0' + FW1 + FW2 + CLIP + PN + 1 zero + 492 bytes BS data +# +# Where PN is packet number 0, 1, 2, ..., 66 +# BS data in format "BS1, BS0, BS1, BS0, ...", MSB is first +# +# 66 packets with PN = 0 - 65 contain 492 bytes each, and 67-th packet with PN = 66 contains +# the remaining 296 bytes of data and junk data to full 492 bytes size +# + +# Define the name of the hardware and the items on the hardware screen (see quisk_conf_defaults.py): +################ Receivers SdrMicron, The SDR Micron project by David Fainitski. +## hardware_file_name Hardware file path, rfile +# This is the file that contains the control logic for each radio. +#hardware_file_name = 'sdrmicronpkg/quisk_hardware.py' + + +class Hardware(BaseHardware): + sample_rates = [48, 96, 192, 240, 384, 480, 640 ,768, 960, 1536, 1920] + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.device = None + self.usb = None + self.rf_gain_labels = ('RF +10', 'RF 0', 'RF -10', 'RF -20') + self.index = 1 + self.enable = 0 + self.rate = 0 + self.att = 10 + self.freq = 7220000 + self.old_freq = 0 + self.sdrmicron_clock = 76800000 + self.sdrmicron_decim = 1600 + self.bscope_data = bytearray(0) + self.fw_ver = None + self.frame_msg = '' + + if conf.fft_size_multiplier == 0: + conf.fft_size_multiplier = 3 # Set size needed by VarDecim + + rx_bytes = 3 # rx_bytes is the number of bytes in each I or Q sample: 1, 2, 3, or 4 + rx_endian = 1 # rx_endian is the order of bytes in the sample array: 0 == little endian; 1 == big endian + self.InitSamples(rx_bytes, rx_endian) # Initialize: read samples from this hardware file and send them to Quisk + bs_bytes = 2 + bs_endian = 1 + self.InitBscope(bs_bytes, bs_endian, self.sdrmicron_clock, 16384) # Initialize bandscope + + def open(self): # This method must return a string showing whether the open succeeded or failed. + enum = d2xx.createDeviceInfoList() # quantity of FTDI devices + if(enum==0): + return 'Device was not found' + for i in range(enum): # Searching and openinq needed device + a = d2xx.getDeviceInfoDetail(i) + if(a['description']==b'SDR-Micron'): + try: self.usb = d2xx.openEx(a['serial']) + except: + return 'Device was not found' + Mode = 64 # Configure FT2232H into 0x40 Sync FIFO Mode + self.usb.setBitMode(255, 0) # reset + time.sleep(0.1) + self.usb.setBitMode(255, Mode) #configure FT2232H into Sync FIFO mode + self.usb.setTimeouts(100, 100) # read, write + self.usb.setLatencyTimer(2) + self.usb.setUSBParameters(32, 32) # in_tx_size, out_tx_size + time.sleep(1.5) # waiting for initialisation device + data = self.usb.read(self.usb.getQueueStatus()) # clean the usb data buffer + self.device = 'Opened' + self.frame_msg = a['description'].decode('utf-8') + ' S/N - ' + a['serial'].decode('utf-8') + return self.frame_msg + return 'Device was not found' + + def close(self): + if(self.usb): + if(self.device=='Opened'): + enable = 0 + self.device = None + self.rx_control_upd() + time.sleep(0.5) + self.usb.setBitMode(255, 0) # reset + self.usb.close() + + def OnButtonRfGain(self, event): + btn = event.GetEventObject() + n = btn.index + self.att = n * 10 + self.rx_control_upd() + + def ChangeFrequency(self, tune, vfo, source='', band='', event=None): + if vfo: + self.freq = (vfo - self.transverter_offset) + if(self.freq!=self.old_freq): + self.old_freq = self.freq + self.rx_control_upd() + return tune, vfo + + def ChangeBand(self, band): + # band is a string: "60", "40", "WWV", etc. + BaseHardware.ChangeBand(self, band) + btn = self.application.BtnRfGain + if btn: + if band in ('160', '80', '60', '40'): + btn.SetLabel('RF -10', True) + elif band in ('20',): + btn.SetLabel('RF 0', True) + else: + btn.SetLabel('RF +10', True) + + def VarDecimGetChoices(self): # Return a list/tuple of strings for the decimation control. + return list(map(str, self.sample_rates)) # convert integer to string + + def VarDecimGetLabel(self): # return a text label for the control + return "Sample rate ksps" + + def VarDecimGetIndex(self): # return the current index + return self.index + + def VarDecimSet(self, index=None): # return sample rate + if index is None: # initial call to set the sample rate before the call to open() + rate = self.application.vardecim_set + try: + self.index = self.sample_rates.index(rate // 1000) + except: + self.index = 0 + else: + self.index = index + rate = self.sample_rates[self.index] * 1000 + self.rate = self.index + if(rate>=960000): + rx_bytes = 2 + rx_endian = 1 + self.InitSamples(rx_bytes, rx_endian) + else: + rx_bytes = 3 + rx_endian = 1 + self.InitSamples(rx_bytes, rx_endian) + self.rx_control_upd() + return rate + + def VarDecimRange(self): # Return the lowest and highest sample rate. + return (48000, 1920000) + + def StartSamples(self): # called by the sound thread + self.enable = 1 + self.rx_control_upd() + self.bscope_control_upd() + + def StopSamples(self): # called by the sound thread + self.enable = 0 + self.rx_control_upd() + self.bscope_control_upd() + + def rx_control_upd(self): + if(self.device=='Opened'): + work = self.freq + freq4 = work & 0xFF + work = work >> 8 + freq3 = work & 0xFF + work = work >> 8 + freq2 = work & 0xFF + work = work >> 8 + freq1 = work & 0xFF + if sys.version_info.major <= 2: + MESSAGE = 7*chr(0x55) + chr(0xd5) + 'RX0' + chr(self.enable) + chr(self.rate) + MESSAGE += chr(freq1) + chr(freq2) + chr(freq3) + chr(freq4) + chr(self.att) + 14*chr(0) + else: + MESSAGE = b"\x55\x55\x55\x55\x55\x55\x55\xd5RX0" + MESSAGE += bytes((self.enable, self.rate, freq1, freq2, freq3, freq4, self.att)) + MESSAGE += bytes(14) + try: self.usb.write(MESSAGE) + except: print('Error while rx_control_upd') + + def bscope_control_upd(self): + if self.device == 'Opened': + if sys.version_info.major <= 2: + MESSAGE = 7*chr(0x55) + chr(0xd5) + 'BS0' + chr(self.enable) + chr(100) + 19 * chr(0) + else: + MESSAGE = b"\x55\x55\x55\x55\x55\x55\x55\xd5BS0" + MESSAGE += bytes((self.enable, 100)) + MESSAGE += bytes(19) + try: self.usb.write(MESSAGE) + except: None + + def GetRxSamples(self): # Read all data from the SDR Micron and process it. + if self.device == None: + return + while (self.usb.getQueueStatus() >= 508): + data = self.usb.read(508) + data = bytearray(data) + if data[8:11] == bytearray(b'RX0'): # Rx I/Q data + if data[13]: + self.GotClip() + if self.fw_ver is None: + self.fw_ver = chr(data[11]) + '.' + chr(data[12]) + self.frame_msg += ' F/W version - ' + self.fw_ver + self.application.main_frame.SetConfigText(self.frame_msg) + self.AddRxSamples(data[16:]) + elif data[8:11] == bytearray(b'BS0'): # bandscope data + packet_number = data[14] + if packet_number == 0: # start of a block of data + self.bscope_data = data[16:] # 492 bytes + elif packet_number < 66: + self.bscope_data += data[16:] # 492 bytes + else: # end of a block of data, 296 bytes + self.bscope_data += data[16:312] + self.AddBscopeSamples(self.bscope_data) + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6c11b9e --- /dev/null +++ b/setup.py @@ -0,0 +1,107 @@ +from setuptools import setup, Extension +import sys +import os + +# This file is used to build the Linux and Mac versions of Quisk. Windows builds are not included. + +# You must define the version here. A title string including +# the version will be written to __init__.py and read by quisk.py. + +Version = '4.2.28' + +fp = open("__init__.py", "w") # write title string +fp.write("#Quisk version %s\n" % Version) +fp.write("from .quisk import main\n") +fp.close() + +sources = ['quisk.c', 'sound.c', 'is_key_down.c', 'microphone.c', 'utility.c', + 'sound_alsa.c', 'sound_pulseaudio.c', 'sound_portaudio.c', 'sound_directx.c', 'sound_wasapi.c', + 'filter.c', 'extdemod.c', 'freedv.c', 'quisk_wdsp.c', 'ac2yd/remote.c'] + +# Afedri hardware support added by Alex, Alex@gmail.com +mAfedri = Extension ('quisk.afedrinet.afedrinet_io', + libraries = ['m'], + sources = ['import_quisk_api.c', 'is_key_down.c', 'afedrinet/afedrinet_io.c'], + include_dirs = ['.'], + ) + +mSoapy = Extension ('quisk.soapypkg.soapy', + libraries = ['m', 'SoapySDR'], + sources = ['import_quisk_api.c', 'soapypkg/soapy.c'], + include_dirs = ['.'], + ) + +# Changes for MacOS support thanks to Mario, DL3LSM. +# Changes for building from macports provided by Eric, KM4DSJ +# Updated code for a Mac build contributed by Christoph, DL1YCF, December 2020. +if sys.platform == "darwin": # Build for Macintosh + define_macros = [("QUISK_HAVE_PORTAUDIO", None)] # PortAudio is always available + libraries = ['portaudio', 'fftw3', 'm'] + if os.path.isdir('/opt/local/include'): # MacPorts + base_dir = '/opt/local' + elif os.path.isdir('/usr/local/include'): # HomeBrew + base_dir = '/usr/local' + else: # Regular build? + base_dir = '/usr' + if os.path.isfile(base_dir + "/include/pulse/pulseaudio.h"): + libraries.append('pulse') + define_macros.append(("QUISK_HAVE_PULSEAUDIO", None)) + Modules = [Extension ('quisk._quisk', include_dirs=['.', base_dir + '/include'], library_dirs=['.', base_dir + '/lib'], + libraries=libraries, sources=sources, define_macros=define_macros)] +elif "freebsd" in sys.platform: #Build for FreeBSD + libraries = ['pulse', 'fftw3', 'm'] + base_dir = '/usr/local' + define_macros = [("QUISK_HAVE_PULSEAUDIO", None)] # Pulseaudio is in FreeBSD base + Modules = [Extension ('quisk._quisk', include_dirs=['.', base_dir + '/include'], library_dirs=['.', base_dir + '/lib'], + libraries=libraries, sources=sources, define_macros=define_macros)] +else: # Linux + define_macros = [("QUISK_HAVE_ALSA", None), ("QUISK_HAVE_PULSEAUDIO", None)] + libraries = ['asound', 'pulse', 'fftw3', 'm'] + if os.path.isfile("/usr/include/portaudio.h"): + libraries.append('portaudio') + define_macros.append(("QUISK_HAVE_PORTAUDIO", None)) + Modules = [Extension ('quisk._quisk', libraries=libraries, sources=sources, define_macros=define_macros)] + Modules.append(mAfedri) + if os.path.isdir("/usr/include/SoapySDR") or os.path.isdir("/usr/local/include/SoapySDR"): + Modules.append(mSoapy) + +setup (name = 'quisk', + version = Version, + description = 'QUISK is a Software Defined Radio (SDR) transceiver that can control various radio hardware.', + long_description = """QUISK is a Software Defined Radio (SDR) transceiver. +You supply radio hardware that converts signals at the antenna to complex (I/Q) data at an +intermediate frequency (IF). Data can come from a sound card, Ethernet or USB. Quisk then filters and +demodulates the data and sends the audio to your speakers or headphones. For transmit, Quisk takes +the microphone signal, converts it to I/Q data and sends it to the hardware. + +Quisk can be used with SoftRock, Hermes Lite 2, HiQSDR, Odyssey and many radios that use the Hermes protocol. +Quisk can connect to digital programs like Fldigi and WSJT-X. Quisk can be connected to other software like +N1MM+ and software that uses Hamlib. +""", + author = 'James C. Ahlstrom', + author_email = 'jahlstr@gmail.com', + url = 'http://james.ahlstrom.name/quisk/', + packages = ['quisk', 'quisk.n2adr', 'quisk.softrock', 'quisk.freedvpkg', + 'quisk.hermes', 'quisk.hiqsdr', 'quisk.afedrinet', 'quisk.soapypkg', + 'quisk.sdrmicronpkg', 'quisk.perseuspkg', 'quisk.ac2yd'], + package_dir = {'quisk' : '.'}, + package_data = {'' : ['*.txt', '*.html', '*.so', '*.dll']}, + entry_points = {'gui_scripts' : ['quisk = quisk.quisk:main', 'quisk_vna = quisk.quisk_vna:main']}, + ext_modules = Modules, + provides = ['quisk'], + classifiers = [ + 'Development Status :: 6 - Mature', + 'Environment :: X11 Applications', + 'Environment :: Win32 (MS Windows)', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Natural Language :: English', + 'Operating System :: POSIX :: Linux', + 'Operating System :: Microsoft :: Windows', + 'Programming Language :: Python :: 3', + 'Programming Language :: C', + 'Topic :: Communications :: Ham Radio', + ], +) + + diff --git a/soapypkg/__init__.py b/soapypkg/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/soapypkg/__init__.py @@ -0,0 +1 @@ +# diff --git a/soapypkg/makefile b/soapypkg/makefile new file mode 100644 index 0000000..5847e6a --- /dev/null +++ b/soapypkg/makefile @@ -0,0 +1,7 @@ + +soapy2: + python2 setup.py build_ext --force --inplace + +soapy3: + python3 setup.py build_ext --force --inplace + diff --git a/soapypkg/quisk_hardware.py b/soapypkg/quisk_hardware.py new file mode 100644 index 0000000..ba4c5c1 --- /dev/null +++ b/soapypkg/quisk_hardware.py @@ -0,0 +1,161 @@ +# This is the hardware file to support radios accessed by the SoapySDR interface. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import socket, traceback, time, math +import _quisk as QS +try: + from soapypkg import soapy +except: + #traceback.print_exc() + soapy = None + +from quisk_hardware_model import Hardware as BaseHardware + +DEBUG = 0 + +class Hardware(BaseHardware): + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.vardecim_index = 0 + self.fVFO = 0.0 # Careful, this is a float + def pre_open(self): + pass + def set_parameter(self, *args): + if soapy: + txt = soapy.set_parameter(*args) + if txt: + dlg = wx.MessageDialog(None, txt, 'SoapySDR Error', wx.OK|wx.ICON_ERROR) + dlg.ShowModal() + dlg.Destroy() + def open(self): # Called once to open the Hardware + if not soapy: + return "Soapy module not available" + radio_dict = self.application.local_conf.GetRadioDict() + device = radio_dict.get('soapy_device', '') + if radio_dict.get('soapy_enable_tx', '') == "Enable": + txt = soapy.open_device(device, 1, self.conf.data_poll_usec) + else: + txt = soapy.open_device(device, 3, self.conf.data_poll_usec) # Tx is disabled + for name in ('soapy_setAntenna_rx', 'soapy_setAntenna_tx'): + value = radio_dict.get(name, '') # string values + self.set_parameter(name, value, 0.0) + for name in ('soapy_setBandwidth_rx', 'soapy_setBandwidth_tx', 'soapy_setSampleRate_rx', 'soapy_setSampleRate_tx'): + value = radio_dict.get(name, '') + try: + value = float(value) * 1E3 # these are in KHz + except: + pass + else: + self.set_parameter(name, '', value) + if name == 'soapy_setSampleRate_tx': + value = int(value + 0.1) + QS.set_tx_audio(tx_sample_rate=value) + self.ChangeGain('_rx') + self.ChangeGain('_tx') + #for name in ('soapy_getSampleRate_rx', 'soapy_getSampleRate_tx', 'soapy_getBandwidth_rx', 'soapy_getBandwidth_tx'): + # print ('Get ***', name, soapy.get_parameter(name, 1)) + return txt + def ChangeGain(self, rxtx): # rxtx is '_rx' or '_tx' + if not soapy: + return + radio_dict = self.application.local_conf.GetRadioDict() + gain_mode = radio_dict['soapy_gain_mode' + rxtx] + gain_values = radio_dict['soapy_gain_values' + rxtx] + if gain_mode == 'automatic': + self.set_parameter('soapy_setGainMode' + rxtx, 'true', 0.0) + elif gain_mode == 'total': + self.set_parameter('soapy_setGainMode' + rxtx, 'false', 0.0) + gain = gain_values.get('total', '0') + gain = float(gain) + self.set_parameter('soapy_setGain' + rxtx, '', gain) + elif gain_mode == 'detailed': + self.set_parameter('soapy_setGainMode' + rxtx, 'false', 0.0) + for name, dmin, dmax, dstep in radio_dict.get('soapy_listGainsValues' + rxtx, ()): + if name == 'total': + continue + gain = gain_values.get(name, '0') + gain = float(gain) + self.set_parameter('soapy_setGainElement' + rxtx, name, gain) + def close(self): # Called once to close the Hardware + if soapy: + soapy.close_device(1) + def ChangeFrequency(self, tune, vfo, source='', band='', event=None): + fVFO = float(vfo - self.transverter_offset) + if self.fVFO != fVFO: + self.fVFO = fVFO + self.set_parameter('soapy_setFrequency_rx', '', fVFO) + self.set_parameter('soapy_setFrequency_tx', '', float(tune - self.transverter_offset)) + return tune, vfo + def ReturnFrequency(self): + # Return the current tuning and VFO frequency. If neither have changed, + # you can return (None, None). This is called at about 10 Hz by the main. + # return (tune, vfo) # return changed frequencies + return None, None # frequencies have not changed + def ReturnVfoFloat(self): + # Return the accurate VFO frequency as a floating point number. + # You can return None to indicate that the integer VFO frequency is valid. + return None + def OnBtnFDX(self, fdx): # fdx is 0 or 1 + if soapy: + self.set_parameter('soapy_FDX', '', float(fdx)) + def OnButtonPTT(self, event): + btn = event.GetEventObject() + if btn.GetValue(): + QS.set_PTT(1) + QS.set_key_down(1) + else: + QS.set_PTT(0) + QS.set_key_down(0) + def OnSpot(self, level): + # level is -1 for Spot button Off; else the Spot level 0 to 1000. + pass + def ChangeMode(self, mode): # Change the tx/rx mode + # mode is a string: "USB", "AM", etc. + pass + def ChangeBand(self, band): + # band is a string: "60", "40", "WWV", etc. + BaseHardware.ChangeBand(self, band) + def HeartBeat(self): # Called at about 10 Hz by the main + pass + def ImmediateChange(self, name, value): + if name in ('soapy_gain_mode_rx', 'soapy_gain_mode_tx'): + self.ChangeGain(name[-3:]) + elif name in ('soapy_setAntenna_rx', 'soapy_setAntenna_tx'): + self.set_parameter(name, value, 0.0) # string values + elif name in ('soapy_setBandwidth_rx', 'soapy_setBandwidth_tx', 'soapy_setSampleRate_rx', 'soapy_setSampleRate_tx'): + try: + value = float(value) * 1E3 # kHz values + except: + pass + else: + self.set_parameter(name, '', value) + if name == 'soapy_setSampleRate_tx': + value = int(value + 0.1) + QS.set_tx_audio(tx_sample_rate=value) + elif name == 'soapy_setSampleRate_rx': + value = int(value + 0.1) + self.application.OnBtnDecimation(rate=value) + self.set_parameter('soapy_setFrequency_rx', '', self.fVFO) # driver Lime requires reset of Rx freq on sample rate change + # The "VarDecim" methods are used to change the hardware decimation rate. + # If VarDecimGetChoices() returns any False value, no other methods are called. + def VarDecimGetChoices(self): # Not used to set sample rate + return ["None"] + def VarDecimGetLabel(self): # Return a text label for the decimation control. + return 'Rx rate: Use SoapySDR config' + def VarDecimGetIndex(self): # Return the index 0, 1, ... of the current decimation. + return 0 + def VarDecimSet(self, index=None): # Called when the control is operated; if index==None, called on startup. + name = 'soapy_setSampleRate_rx' + radio_dict = self.application.local_conf.GetRadioDict() + rate = radio_dict.get(name, 48) # this is in KHz + try: + rate = float(rate) * 1E3 + rate = int(rate + 0.1) + except: + rate = 48000 + return rate + def VarDecimRange(self): # Return the lowest and highest sample rate. + return 48000, 192000 diff --git a/soapypkg/setup.py b/soapypkg/setup.py new file mode 100644 index 0000000..3b7ac19 --- /dev/null +++ b/soapypkg/setup.py @@ -0,0 +1,50 @@ +from distutils.core import setup, Extension +import sys + +module2 = Extension ('soapy', + libraries = ['m', 'SoapySDR'], + sources = ['../import_quisk_api.c', 'soapy.c'], + include_dirs = ['.', '..'], + ) + +modulew2 = Extension ('soapy', + sources = ['../import_quisk_api.c', 'soapy.c'], + include_dirs = ['.', '..'], + libraries = ['WS2_32', 'SoapySDR'], + ) + +if sys.platform == "win32": + Modules = [modulew2] +else: + Modules = [module2] + +setup (name = 'soapy', + version = '0.1', + description = 'soapy is an extension to Quisk to support hardware using the SoapySDR API', + long_description = """SoapySDR is a layer of software that can connect to various SDR +hardware. It provides a standard API to a client program. By using the SoapySDR API, Quisk can +connect to all the hardware devices that SoapySDR supports. +""", + author = 'James C. Ahlstrom', + author_email = 'jahlstr@gmail.com', + url = 'http://james.ahlstrom.name/quisk/soapy.html', + download_url = 'http://james.ahlstrom.name/quisk/', + packages = ['quisk.soapypkg'], + package_dir = {'soapy' : '.'}, + ext_modules = Modules, + classifiers = [ + 'Development Status :: 6 - Mature', + 'Environment :: X11 Applications', + 'Environment :: Win32 (MS Windows)', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Natural Language :: English', + 'Operating System :: POSIX :: Linux', + 'Operating System :: Microsoft :: Windows', + 'Programming Language :: Python', + 'Programming Language :: C', + 'Topic :: Communications :: Ham Radio', + ], +) + + diff --git a/soapypkg/soapy.c b/soapypkg/soapy.c new file mode 100644 index 0000000..d769b88 --- /dev/null +++ b/soapypkg/soapy.c @@ -0,0 +1,532 @@ +#include +#include +#include +#include +#include + +#define IMPORT_QUISK_API +#include "quisk.h" +#include "filter.h" + +// This module was written by James Ahlstrom, N2ADR. + +// This module uses the Python interface to import symbols from the parent _quisk +// extension module. It must be linked with import_quisk_api.c. See the documentation +// at the start of import_quisk_api.c. + +#define DEBUG 0 + +#define RX_BUF_SIZE (SAMP_BUFFER_SIZE / 2) +static SoapySDRDevice * soapy_sample_device; // device for the sample stream +static SoapySDRDevice * soapy_config_device; // device for configuration, not samples +static SoapySDRStream * rxStream; +static SoapySDRStream * txStream; +static double rx_sample_rate = 48000; +static double tx_sample_rate = 48000; +static complex float rx_stream_buffer[RX_BUF_SIZE]; +static void * rx_stream_buffs[] = {rx_stream_buffer}; +static int shutdown_sample_device; +static int data_poll_usec = 10000; +static int soapy_FDX; // Full duplex flag +static int soapy_KeyDown; // Current key state +static int soapy_KeyWasDown; // Previous key state +static size_t numTxChannels; // number of Tx channels +static size_t txMTU; // maximum transmission unit of Tx stream + +// Start sample capture; called from the sound thread. +static void quisk_start_samples(void) +{ + //setup an Rx and Tx stream (complex floats) +#if SOAPY_SDR_API_VERSION < 0x00080000 + if (SoapySDRDevice_setupStream(soapy_sample_device, &rxStream, SOAPY_SDR_RX, SOAPY_SDR_CF32, NULL, 0, NULL) != 0) { + printf("Soapy Rx setupStream fail: %s\n", SoapySDRDevice_lastError()); + return; + } + if (numTxChannels) { + if (SoapySDRDevice_setupStream(soapy_sample_device, &txStream, SOAPY_SDR_TX, SOAPY_SDR_CF32, NULL, 0, NULL) != 0) { + printf("Soapy Tx setupStream fail: %s\n", SoapySDRDevice_lastError()); + return; + } + txMTU = SoapySDRDevice_getStreamMTU(soapy_sample_device, txStream); + } +#else + if ((rxStream = SoapySDRDevice_setupStream(soapy_sample_device, SOAPY_SDR_RX, SOAPY_SDR_CF32, NULL, 0, NULL)) == NULL) { + printf("Soapy Rx setupStream fail: %s\n", SoapySDRDevice_lastError()); + return; + } + if (numTxChannels) { + if ((txStream = SoapySDRDevice_setupStream(soapy_sample_device, SOAPY_SDR_TX, SOAPY_SDR_CF32, NULL, 0, NULL)) == NULL) { + printf("Soapy Tx setupStream fail: %s\n", SoapySDRDevice_lastError()); + return; + } + txMTU = SoapySDRDevice_getStreamMTU(soapy_sample_device, txStream); + } +#endif + SoapySDRDevice_activateStream(soapy_sample_device, rxStream, 0, 0, 0); //start streaming +} + +// Stop sample capture; called from the sound thread. +static void quisk_stop_samples(void) +{ + shutdown_sample_device = 1; + if (rxStream) { + SoapySDRDevice_deactivateStream(soapy_sample_device, rxStream, 0, 0); //stop streaming + SoapySDRDevice_closeStream(soapy_sample_device, rxStream); + rxStream = NULL; + } + if (txStream) { + SoapySDRDevice_deactivateStream(soapy_sample_device, txStream, 0, 0); //stop streaming + SoapySDRDevice_closeStream(soapy_sample_device, txStream); + txStream = NULL; + } +} + +// Called in a loop to read samples; called from the sound thread. +static int quisk_read_samples(complex double * cSamples) +{ + int i, flags; //flags set by receive operation + long long timeNs; //timestamp for receive buffer + int nSamples; + int num_samp; + + soapy_KeyDown = quisk_is_key_down(); + num_samp = (int)(rx_sample_rate * (data_poll_usec * 1E-6)); + num_samp = ((num_samp + 255) / 256) * 256; + if (num_samp > RX_BUF_SIZE) + num_samp = RX_BUF_SIZE; + if (shutdown_sample_device) { + if (rxStream) { + quisk_stop_samples(); + } + if (soapy_sample_device) { + SoapySDRDevice_unmake(soapy_sample_device); + soapy_sample_device = NULL; + } + nSamples = num_samp; + for (i = 0; i < nSamples; i++) + cSamples[i] = 0; + } + else if (rxStream) { + nSamples = SoapySDRDevice_readStream(soapy_sample_device, rxStream, rx_stream_buffs, num_samp, &flags, &timeNs, data_poll_usec * 2); + if (nSamples == -1) { // Timeout + nSamples = 0; + } + else if (nSamples < 0) { // Some other error + pt_quisk_sound_state->read_error++; + nSamples = 0; + } + pt_quisk_sound_state->latencyCapt = 0; + for (i = 0; i < nSamples; i++) + cSamples[i] = rx_stream_buffer[i] * CLIP32; + } + else { + nSamples = num_samp; + for (i = 0; i < nSamples; i++) + cSamples[i] = 0; + } + return nSamples; // return number of samples +} + +// Called in a loop to write samples; called from the sound thread. +static int quisk_write_samples(complex double * cSamples, int nSamples) +{ + static complex float * tx_stream_buffer = NULL; + static int tx_buf_size = 0; + static long long timeNs = 0; + int i, flags, ret, count; + + if ( ! txStream) + return 0; + if (soapy_KeyDown != soapy_KeyWasDown) { // key changed state + soapy_KeyWasDown = soapy_KeyDown; + if (DEBUG) + printf("**** Key Change %i rate %.0f\n", soapy_KeyDown, tx_sample_rate); + if (soapy_KeyDown) // Key went down + SoapySDRDevice_activateStream(soapy_sample_device, txStream, 0, 0, 0); // start Tx streaming + else // Key went up + SoapySDRDevice_deactivateStream(soapy_sample_device, txStream, 0, 0); // stop Tx streaming + } + if ( ! soapy_KeyDown || nSamples <= 0) + return 0; + + if (tx_buf_size < nSamples) { + if (tx_stream_buffer) + free(tx_stream_buffer); + tx_buf_size = nSamples * 2; + tx_stream_buffer = (complex float *)malloc(tx_buf_size * sizeof(complex float)); + } + for (i = 0; i < nSamples; i++) + tx_stream_buffer[i] = cSamples[i] / CLIP16; + while (nSamples > 0) { + if (nSamples > (int)txMTU) + count = txMTU; + else + count = nSamples; + nSamples -= count; + timeNs = 0; //+= count / tx_sample_rate * 1E9; + ret = SoapySDRDevice_writeStream(soapy_sample_device, txStream, (void *)&tx_stream_buffer, count, &flags, timeNs, data_poll_usec * 2); + if (ret < 0) + printf("Soapy writeStream fail: %s\n", SoapySDRDevice_lastError()); + if (ret != count) + printf ("Soapy writeStream short write; %d < %d\n", ret, count); + //printf ("Soapy writeStream write; %i %i\n", ret, count); + } + return 0; +} + +// Called to close the sample source; called from the GUI thread. +static PyObject * close_device(PyObject * self, PyObject * args) +{ + int sample_device; + + if (!PyArg_ParseTuple (args, "i", &sample_device)) + return NULL; + if (sample_device) { + shutdown_sample_device = 1; + } + else { + if (soapy_config_device) { + SoapySDRDevice_unmake(soapy_config_device); + soapy_config_device = NULL; + } + } + Py_INCREF (Py_None); + return Py_None; +} + +// Called to open the SoapySDR device; called from the GUI thread. +static PyObject * open_device(PyObject * self, PyObject * args) +{ + int sample_device, poll; + const char * name; + char buf128[128]; + SoapySDRDevice * sdev; + + if (!PyArg_ParseTuple (args, "sii", &name, &sample_device, &poll)) + return NULL; + sdev = SoapySDRDevice_makeStrArgs(name); + if(sdev) { + snprintf(buf128, 128, "Capture from %s", name); + if (sample_device) { + shutdown_sample_device = 0; + soapy_sample_device = sdev; + data_poll_usec = poll; + quisk_sample_source4(&quisk_start_samples, &quisk_stop_samples, &quisk_read_samples, &quisk_write_samples); + numTxChannels = SoapySDRDevice_getNumChannels(sdev, SOAPY_SDR_TX); + if (sample_device == 3) // disable transmit + numTxChannels = 0; + } + else { + soapy_config_device = sdev; + } + } + else { + snprintf(buf128, 128, "SoapySDRDevice_make fail: %s", SoapySDRDevice_lastError()); + } + return PyString_FromString(buf128); +} + +static void get_direc_len(const char * name, int * direction, int * length) +{ // return the direction (Rx or Tx) and length of name to compare + *length = strlen(name); + *direction = SOAPY_SDR_RX; + if (*length < 4) + return; + if (name[*length - 1] == 'x' && name[*length - 3] == '_') { // ends in "_rx" or "_tx" + if (name[*length - 2] == 't') + *direction = SOAPY_SDR_TX; + *length -= 3; + } +} + +// Get a list of SoapySDR devices +static PyObject * get_device_list(PyObject * self, PyObject * args) // Called from GUI thread +{ + PyObject * devices; + PyObject * dict; + size_t length, i, j; + SoapySDRKwargs * results; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + devices = PyList_New(0); + results = SoapySDRDevice_enumerate(NULL, &length); + for (i = 0; i < length; i++) { + dict = PyDict_New(); + for (j = 0; j < results[i].size; j++) + PyDict_SetItemString(dict, results[i].keys[j], PyString_FromString(results[i].vals[j])); + PyList_Append(devices, dict); + Py_DECREF(dict); + } + SoapySDRKwargsList_clear(results, length); + return devices; +} + +static PyObject * set_parameter(PyObject * self, PyObject * args) // Called from GUI thread +{ // Parameter name can end in "_rx" or "_tx" to specify direction. + int direction, length; + const char * param; // name of the parameter + const char * name2; // string data or sub-parameter name if any + double datum; // floating point value if any + bool is_true; + char msg200[200]; + + if (!PyArg_ParseTuple (args, "ssd", ¶m, &name2, &datum)) + return NULL; + if (DEBUG) + printf ("Set %s - %s - %lf\n", param, name2, datum); + get_direc_len(param, &direction, &length); + msg200[0] = 0; + if (soapy_sample_device) { + if (numTxChannels <= 0 && direction == SOAPY_SDR_TX) { + } + //else if (direction == SOAPY_SDR_TX) { + //} + else if ( ! strcmp(param, "soapy_FDX")) { + if (datum) + soapy_FDX = 1; + else + soapy_FDX = 0; + } + else if ( ! strncmp(param, "soapy_setAntenna", length)) { // do not set empty string + if (name2[0] && SoapySDRDevice_setAntenna(soapy_sample_device, direction, 0, name2) != 0) + snprintf(msg200, 200, "%s fail: %s\n", param, SoapySDRDevice_lastError()); + } + else if ( ! strncmp(param, "soapy_setBandwidth", length)) { + if (soapy_sample_device && SoapySDRDevice_setBandwidth(soapy_sample_device, direction, 0, datum) != 0) + snprintf(msg200, 200, "%s fail: %s\n", param, SoapySDRDevice_lastError()); + } + else if ( ! strncmp(param, "soapy_setFrequency", length)) { + if (SoapySDRDevice_setFrequency(soapy_sample_device, direction, 0, datum, NULL) != 0) + snprintf(msg200, 200, "%s fail: %s\n", param, SoapySDRDevice_lastError()); + } + else if ( ! strncmp(param, "soapy_setGain", length)) { + if (soapy_sample_device && SoapySDRDevice_setGain(soapy_sample_device, direction, 0, datum) != 0) + snprintf(msg200, 200, "%s fail: %s\n", param, SoapySDRDevice_lastError()); + } + else if ( ! strncmp(param, "soapy_setGainElement", length)) { + if (SoapySDRDevice_setGainElement(soapy_sample_device, direction, 0, name2, datum) != 0) + snprintf(msg200, 200, "%s fail: %s\n", param, SoapySDRDevice_lastError()); + } + else if ( ! strncmp(param, "soapy_setGainMode", length)) { + if ( ! strcmp(name2, "true")) + is_true = 1; + else + is_true = 0; + if (SoapySDRDevice_setGainMode(soapy_sample_device, direction, 0, is_true) != 0) + snprintf(msg200, 200, "%s fail: %s\n", param, SoapySDRDevice_lastError()); + } + else if ( ! strncmp(param, "soapy_setSampleRate", length)) { + if (direction == SOAPY_SDR_RX) + rx_sample_rate = datum; + else + tx_sample_rate = datum; + if (SoapySDRDevice_setSampleRate(soapy_sample_device, direction, 0, datum) != 0) + snprintf(msg200, 200, "%s fail: %s\n", param, SoapySDRDevice_lastError()); + } + else { + snprintf(msg200, 200, "Soapy set_parameter() for unknown name %s\n", param); + } + } + if (msg200[0]) + return PyString_FromString(msg200); + Py_INCREF (Py_None); + return Py_None; +} + +static void Range2List(SoapySDRRange range, PyObject * pylist) +{ + PyObject * pyobj; + + pyobj = PyFloat_FromDouble(range.minimum); + PyList_Append(pylist, pyobj); + Py_DECREF(pyobj); + pyobj = PyFloat_FromDouble(range.maximum); + PyList_Append(pylist, pyobj); + Py_DECREF(pyobj); + pyobj = PyFloat_FromDouble(range.step); + PyList_Append(pylist, pyobj); + Py_DECREF(pyobj); +} + +static PyObject * get_parameter(PyObject * self, PyObject * args) // Called from GUI thread +{ // Return a SoapySDR parameter. + // Parameter name can end in "_rx" or "_tx" to specify direction. + int sample_device, direction, length; + char * name; + char ** names; + size_t i, len_list; + bool is_true; + double value; + PyObject * pylist, * pyobj, * pylst2; + SoapySDRDevice * sdev; + SoapySDRRange range; + SoapySDRRange * ranges; + + if (!PyArg_ParseTuple (args, "si", &name, &sample_device)) + return NULL; + if (sample_device) + sdev = soapy_sample_device; + else + sdev = soapy_config_device; + get_direc_len(name, &direction, &length); + if ( ! sdev) { + ; + } + else if ( ! strncmp(name, "soapy_listAntennas", length)) { + pylist = PyList_New(0); + names = SoapySDRDevice_listAntennas(sdev, direction, 0, &len_list); + for (i = 0; i < len_list; i++) { + pyobj = PyString_FromString(names[i]); + PyList_Append(pylist, pyobj); + Py_DECREF(pyobj); + } + SoapySDRStrings_clear(&names, len_list); + return pylist; + } + else if ( ! strncmp(name, "soapy_getBandwidth", length)) { + value = SoapySDRDevice_getBandwidth(sdev, direction, 0); + return PyFloat_FromDouble(value); + } + else if ( ! strncmp(name, "soapy_getBandwidthRange", length)) { + pylist = PyList_New(0); + ranges = SoapySDRDevice_getBandwidthRange(sdev, direction, 0, &len_list); + for (i = 0; i < len_list; i++) { + pylst2 = PyList_New(0); + range = ranges[i]; + Range2List(range, pylst2); + PyList_Append(pylist, pylst2); + Py_DECREF(pylst2); + } + return pylist; + } + else if ( ! strncmp(name, "soapy_getFullDuplex", length)) { + is_true = SoapySDRDevice_getFullDuplex(sdev, direction, 0); + if (is_true) + return PyInt_FromLong(1); + else + return PyInt_FromLong(0); + } + else if ( ! strncmp(name, "soapy_getGainRange", length)) { + pylist = PyList_New(0); + range = SoapySDRDevice_getGainRange(sdev, direction, 0); + Range2List(range, pylist); + return pylist; + } + else if ( ! strncmp(name, "soapy_getSampleRate", length)) { + value = SoapySDRDevice_getSampleRate(sdev, direction, 0); + return PyFloat_FromDouble(value); + } + else if ( ! strncmp(name, "soapy_getSampleRateRange", length)) { + pylist = PyList_New(0); + ranges = SoapySDRDevice_getSampleRateRange(sdev, direction, 0, &len_list); + for (i = 0; i < len_list; i++) { + pylst2 = PyList_New(0); + range = ranges[i]; + Range2List(range, pylst2); + PyList_Append(pylist, pylst2); + Py_DECREF(pylst2); + } + return pylist; + } + else if ( ! strncmp(name, "soapy_hasGainMode", length)) { + is_true = SoapySDRDevice_hasGainMode(sdev, direction, 0); + if (is_true) + return PyInt_FromLong(1); + else + return PyInt_FromLong(0); + } + else if ( ! strncmp(name, "soapy_listGains", length)) { + pylist = PyList_New(0); + names = SoapySDRDevice_listGains(sdev, direction, 0, &len_list); + for (i = 0; i < len_list; i++) { + pyobj = PyString_FromString(names[i]); + PyList_Append(pylist, pyobj); + Py_DECREF(pyobj); + } + SoapySDRStrings_clear(&names, len_list); + return pylist; + } + else if ( ! strncmp(name, "soapy_listGainsValues", length)) { + pylist = PyList_New(0); + pylst2 = PyList_New(0); // First element is the total gain + pyobj = PyString_FromString("total"); + PyList_Append(pylst2, pyobj); + Py_DECREF(pyobj); + range = SoapySDRDevice_getGainRange(sdev, direction, 0); + Range2List(range, pylst2); + PyList_Append(pylist, pylst2); + Py_DECREF(pylst2); + names = SoapySDRDevice_listGains(sdev, direction, 0, &len_list); + for (i = 0; i < len_list; i++) { + pylst2 = PyList_New(0); + pyobj = PyString_FromString(names[i]); + PyList_Append(pylst2, pyobj); + Py_DECREF(pyobj); + range = SoapySDRDevice_getGainElementRange(sdev, direction, 0, names[i]); + Range2List(range, pylst2); + PyList_Append(pylist, pylst2); + Py_DECREF(pylst2); + } + SoapySDRStrings_clear(&names, len_list); + return pylist; + } + else { + printf("Soapy get_parameter() for unknown name %s\n", name); + } + Py_INCREF (Py_None); + return Py_None; +} + +// Functions callable from Python are listed here: +static PyMethodDef QuiskMethods[] = { + {"open_device", open_device, METH_VARARGS, "Open the hardware."}, + {"close_device", close_device, METH_VARARGS, "Close the hardware"}, + {"get_device_list", get_device_list, METH_VARARGS, "Return a list of SoapySDR devices"}, + {"get_parameter", get_parameter, METH_VARARGS, "Get a SoapySDR parameter"}, + {"set_parameter", set_parameter, METH_VARARGS, "Set a SoapySDR parameter"}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +#if PY_MAJOR_VERSION < 3 +// Python 2.7: +// Initialization, and registration of public symbol "initsoapy": +PyMODINIT_FUNC initsoapy (void) +{ + if (Py_InitModule ("soapy", QuiskMethods) == NULL) { + printf("Py_InitModule failed!\n"); + return; + } + // Import pointers to functions and variables from module _quisk + if (import_quisk_api()) { + printf("Failure to import pointers from _quisk\n"); + return; //Error + } +} + +// Python 3: +#else +static struct PyModuleDef soapymodule = { + PyModuleDef_HEAD_INIT, + "soapy", + NULL, + -1, + QuiskMethods +} ; + +PyMODINIT_FUNC PyInit_soapy(void) +{ + PyObject * m; + + m = PyModule_Create(&soapymodule); + if (m == NULL) + return NULL; + + // Import pointers to functions and variables from module _quisk + if (import_quisk_api()) { + printf("Failure to import pointers from _quisk\n"); + return m; //Error + } + return m; +} +#endif diff --git a/softrock/__init__.py b/softrock/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/softrock/__init__.py @@ -0,0 +1 @@ +# diff --git a/softrock/hardware_net.py b/softrock/hardware_net.py new file mode 100644 index 0000000..4caf13b --- /dev/null +++ b/softrock/hardware_net.py @@ -0,0 +1,105 @@ + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import threading, time, math, socket, re +from quisk_hardware_model import Hardware as BaseHardware + +DEBUG = 1 + +import _quisk as QS + +class Hardware(BaseHardware): + def __init__(self, app, conf): + BaseHardware.__init__(self, app, conf) + self.vfo = None + self.ptt_button = 0 + self.usbsr_ip_address = conf.usbsr_ip_address + self.usbsr_port = conf.usbsr_port + + def open(self): # Called once to open the Hardware + freq = self.GetFreq() + if freq: + print ('Run freq', freq) + text = "found usbsoftrock daemon" + else: + print ('cannot find usbsoftrock daemon') + text = "cannot find usbsoftrock daemon" + return text + + def close(self): + pass + def ChangeFrequency(self, tune, vfo, source='', band='', event=None): + if self.vfo != vfo: + self.SetFreq(vfo) + self.vfo = vfo + return tune, vfo + + def ReturnFrequency(self): + # Return the current tuning and VFO frequency. If neither have changed, + # you can return (None, None). This is called at about 10 Hz by the main. + # return (tune, vfo) # return changed frequencies + return None, None # frequencies have not changed + + def ChangeBand(self, band): + # band is a string: "60", "40", "WWV", etc. + pass + + def HeartBeat(self): # Called at about 10 Hz by the main + pass + + def GetFreq(self): # return the running frequency + MESSAGE = "get freq" + srsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + srsock.settimeout(1) + srsock.sendto(MESSAGE.encode('utf-8', errors='ignore'), (self.usbsr_ip_address, self.usbsr_port)) + try: + data, addr = srsock.recvfrom(1024) # buffer size is 1024 bytes + data = data.decode('utf-8', errors='replace') + except: + srsock.close() + print ('error') + return None #maybe return None instead to simplify if statement + else: + srsock.close() + print ('received data', data) + freq = float(re.findall("\d+.\d+", data)[0]) + freq = int(freq * 1.0e6) + return freq + + def SetFreq(self, freq): + if freq <= 0 or freq > 30000000: + return + freq = freq/float(1.0e6) + MESSAGE = "set freq " + str(freq) + srsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + srsock.sendto(MESSAGE.encode('utf-8', errors='ignore'), (self.usbsr_ip_address, self.usbsr_port)) + print (MESSAGE) + return True + + def OnButtonPTT(self, event=None): + if event: + if event.GetEventObject().GetValue(): + self.ptt_button = 1 + message = "set ptt on" + else: + self.ptt_button = 0 + message = "set ptt off" + srsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + srsock.settimeout(1) + srsock.sendto(message.encode('utf-8', errors='ignore'), (self.usbsr_ip_address, self.usbsr_port)) + data, addr = srsock.recvfrom(1024) # buffer size is 1024 bytes + data = data.decode('utf-8', errors='replace') + srsock.close() + print (data) + if data == "ok": + QS.set_key_down(self.ptt_button) + else: + print ('error doing', message) + text = "error setting ptt on or off!" + self.config_text = text + def OnSpot(self, level): + self.spot_level = level + + diff --git a/softrock/hardware_usb.py b/softrock/hardware_usb.py new file mode 100644 index 0000000..c231f18 --- /dev/null +++ b/softrock/hardware_usb.py @@ -0,0 +1,288 @@ +# Please do not change this hardware control module for Quisk. +# It provides USB control of SoftRock hardware. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import sys, struct, time, traceback, math +from quisk_hardware_model import Hardware as BaseHardware +import _quisk as QS + +# All USB access is through control transfers using pyusb. +# byte_array = dev.ctrl_transfer (IN, bmRequest, wValue, wIndex, length, timeout) +# len(string_msg) = dev.ctrl_transfer (OUT, bmRequest, wValue, wIndex, string_msg, timeout) + +try: + import usb + import usb.core, usb.util +except: + if sys.platform == 'win32': + dlg = wx.MessageDialog(None, "The Python pyusb module is required but not installed. Do you want me to install it?", + "Install Python pyusb", style = wx.YES|wx.NO) + if dlg.ShowModal() == wx.ID_YES: + import subprocess + subprocess.call([sys.executable, "-m", "pip", "install", "pyusb"]) + try: + import usb + import usb.core, usb.util + except: + dlg = wx.MessageDialog(None, "Installation of Python pyusb failed. Please install it by hand.", + "Installation failed", style=wx.OK) + dlg.ShowModal() + else: + dlg = wx.MessageDialog(None, "The Python pyusb module is required but not installed. Please install package python-usb.", + "Install Python pyusb", style = wx.OK) + dlg.ShowModal() + +DEBUG = 0 + +# Thanks to Ethan Blanton, KB8OJH, for this patch for the Si570 (many SoftRocks): +# These are used by SetFreqByDirect(); see below. +# The Si570 DCO must be clamped between these values +SI570_MIN_DCO = 4.85e9 +SI570_MAX_DCO = 5.67e9 +# The Si570 has 6 valid HSDIV values. Subtract 4 from HSDIV before +# stuffing it. We want to find the highest HSDIV first, so start +# from 11. +SI570_HSDIV_VALUES = [11, 9, 7, 6, 5, 4] + +IN = usb.util.build_request_type(usb.util.CTRL_IN, usb.util.CTRL_TYPE_VENDOR, usb.util.CTRL_RECIPIENT_DEVICE) +OUT = usb.util.build_request_type(usb.util.CTRL_OUT, usb.util.CTRL_TYPE_VENDOR, usb.util.CTRL_RECIPIENT_DEVICE) + +UBYTE2 = struct.Struct(' 50: + sound = sound[0:30] + '|||' + sound[-17:] + text = 'Capture from SoftRock USB on %s, Firmware %s' % (sound, ver) + #self.application.bottom_widgets.info_text.SetLabel(text) + if DEBUG and usb_dev: + print ('Startup freq', self.GetStartupFreq()) + print ('Run freq', self.GetFreq()) + print ('Address 0x%X' % usb_dev.ctrl_transfer(IN, 0x41, 0, 0, 1)[0]) + sm = usb_dev.ctrl_transfer(IN, 0x3B, 0, 0, 2) + sm = UBYTE2.unpack(sm)[0] + print ('Smooth tune', sm) + return text + def close(self): # Called once to close the Hardware + pass + def ChangeFrequency(self, tune, vfo, source='', band='', event=None): + if self.usb_dev and self.vfo != vfo: + if self.conf.si570_direct_control: + if self.SetFreqByDirect(vfo - self.transverter_offset): + self.vfo = vfo + elif self.SetFreqByValue(vfo - self.transverter_offset): + self.vfo = vfo + if DEBUG: + print ('Change to', vfo) + print ('Run freq', self.GetFreq()) + return tune, vfo + def ChangeBand(self, band): + # band is a string: "60", "40", "WWV", etc. + BaseHardware.ChangeBand(self, band) + self.band = band + self.SetTxLevel() + def ChangeMode(self, mode): + # mode is a string: "USB", "AM", etc. + BaseHardware.ChangeMode(self, mode) + self.mode = mode + QS.set_cwkey(0) + self.SetTxLevel() + def SetTxLevel(self): + tx_level = self.conf.tx_level.get(self.band, 70) + if self.mode[0:3] in ('DGT', 'FDV'): # Digital modes; change power by a percentage + reduc = self.application.digital_tx_level + else: + reduc = self.application.tx_level + tx_level = int(tx_level * reduc / 100.0 + 0.5) + if tx_level < 0: + tx_level = 0 + elif tx_level > 100: + tx_level = 100 + QS.set_mic_out_volume(tx_level) + if DEBUG: print("Change tx_level to", tx_level) + def ReturnFrequency(self): + # Return the current tuning and VFO frequency. If neither have changed, + # you can return (None, None). This is called at about 10 Hz by the main. + # return (tune, vfo) # return changed frequencies + return None, None # frequencies have not changed + def RepeaterOffset(self, offset=None): # Change frequency for repeater offset during Tx + if offset is None: # Return True if frequency change is complete + if time.time() > self.repeater_time0 + self.repeater_delay: + return True + elif offset == 0: # Change back to the original frequency + if self.repeater_freq is not None: + self.repeater_time0 = time.time() + self.ChangeFrequency(self.repeater_freq, self.repeater_freq, 'repeater') + self.repeater_freq = None + else: # Shift to repeater input frequency + self.repeater_time0 = time.time() + self.repeater_freq = self.vfo + vfo = self.vfo + int(offset * 1000) # Convert kHz to Hz + self.ChangeFrequency(vfo, vfo, 'repeater') + return False + def OnSpot(self, level): + pass + def HeartBeat(self): # Called at about 10 Hz by the main + pass + def OnChangeRxTx(self, is_tx): # Called by Quisk when changing between Rx and Tx. "is_tx" is 0 or 1 + if not self.usb_dev: + return + try: + self.usb_dev.ctrl_transfer(IN, 0x50, is_tx, 0, 3) + except usb.core.USBError: + if DEBUG: traceback.print_exc() + try: + self.usb_dev.ctrl_transfer(IN, 0x50, is_tx, 0, 3) + except usb.core.USBError: + if DEBUG: traceback.print_exc() + else: + if DEBUG: print("OnChangeRxTx", is_tx) + else: + if DEBUG: print("OnChangeRxTx", is_tx) + def GetStartupFreq(self): # return the startup frequency / 4 + if not self.usb_dev: + return 0 + ret = self.usb_dev.ctrl_transfer(IN, 0x3C, 0, 0, 4) + s = ret.tobytes() + freq = UBYTE4.unpack(s)[0] + freq = int(freq * 1.0e6 / 2097152.0 / 4.0 + 0.5) + return freq + def GetFreq(self): # return the running frequency / 4 + if not self.usb_dev: + return 0 + ret = self.usb_dev.ctrl_transfer(IN, 0x3A, 0, 0, 4) + s = ret.tobytes() + freq = UBYTE4.unpack(s)[0] + freq = int(freq * 1.0e6 / 2097152.0 / 4.0 + 0.5) + return freq + def SetFreqByValue(self, freq): + freq = int(freq/1.0e6 * 2097152.0 * 4.0 + 0.5) + if freq <= 0: + return + s = UBYTE4.pack(freq) + try: + self.usb_dev.ctrl_transfer(OUT, 0x32, self.si570_i2c_address + 0x700, 0, s) + except usb.core.USBError: + if DEBUG: traceback.print_exc() + else: + return True + def SetFreqByDirect(self, freq): # Thanks to Ethan Blanton, KB8OJH + if freq == 0.0: + return False + # For now, find the minimum DCO speed that will give us the + # desired frequency; if we're slewing in the future, we want this + # to additionally yield an RFREQ ~= 512. + freq = int(freq * 4) + dco_new = None + hsdiv_new = 0 + n1_new = 0 + for hsdiv in SI570_HSDIV_VALUES: + n1 = int(math.ceil(SI570_MIN_DCO / (freq * hsdiv))) + if n1 < 1: + n1 = 1 + else: + n1 = ((n1 + 1) // 2) * 2 + dco = (freq * 1.0) * hsdiv * n1 + # Since we're starting with max hsdiv, this can only happen if + # freq was larger than we can handle + if n1 > 128: + continue + if dco < SI570_MIN_DCO or dco > SI570_MAX_DCO: + # This really shouldn't happen + continue + if not dco_new or dco < dco_new: + dco_new = dco + hsdiv_new = hsdiv + n1_new = n1 + if not dco_new: + # For some reason, we were unable to calculate a frequency. + # Probably because the frequency requested is outside the range + # of our device. + return False # Failure + rfreq = dco_new / self.conf.si570_xtal_freq + rfreq_int = int(rfreq) + rfreq_frac = int(round((rfreq - rfreq_int) * 2**28)) + # It looks like the DG8SAQ protocol just passes r7-r12 straight + # To the Si570 when given command 0x30. Easy enough. + # n1 is stuffed as n1 - 1, hsdiv is stuffed as hsdiv - 4. + hsdiv_new = hsdiv_new - 4 + n1_new = n1_new - 1 + s = struct.Struct('>BBL').pack((hsdiv_new << 5) + (n1_new >> 2), + ((n1_new & 0x3) << 6) + (rfreq_int >> 4), + ((rfreq_int & 0xf) << 28) + rfreq_frac) + self.usb_dev.ctrl_transfer(OUT, 0x30, self.si570_i2c_address + 0x700, 0, s) + return True # Success + def PollCwKey(self): # Called frequently by Quisk to check the CW key status + if not self.usb_dev: + return + if self.mode not in ('CWU', 'CWL'): + return + try: # Test key up/down state + ret = self.usb_dev.ctrl_transfer(IN, 0x51, 0, 0, 1) + except: + QS.set_cwkey(0) + if DEBUG: traceback.print_exc() + else: + # bit 0x20 is the tip, bit 0x02 is the ring (ring not used) + if ret[0] & 0x20 == 0: # Tip: key is down + QS.set_cwkey(1) + else: # key is up + QS.set_cwkey(0) diff --git a/softrock/widgets_tx.py b/softrock/widgets_tx.py new file mode 100644 index 0000000..fa65cec --- /dev/null +++ b/softrock/widgets_tx.py @@ -0,0 +1,15 @@ +# Please do not change this widgets module for Quisk. Instead copy +# it to your own quisk_widgets.py and make changes there. +# +# This module is used to add extra widgets to the QUISK screen. + +import wx +import _quisk as QS +import math + +class BottomWidgets: # Add extra widgets to the bottom of the screen + def __init__(self, app, hardware, conf, frame, gbs, vertBox): + self.hardware = hardware + #self.info_text = app.QuiskText(frame, 'Info', app.button_height) + #gbs.Add(self.info_text, (4, 0), (1, 27), flag=wx.EXPAND) + diff --git a/softrock_tune_vfo.py b/softrock_tune_vfo.py new file mode 100644 index 0000000..ca78d93 --- /dev/null +++ b/softrock_tune_vfo.py @@ -0,0 +1,19 @@ +# This is a replacement hardware file for the Softrock and similar radios. + +# Normally Quisk will change the VFO (center frequency) by large amounts, and perform +# fine tuning within the returned bandwidth. This hardware file does all tuning with the VFO. +# This creates a constant offset between the VFO and the tuning frequency. Specify this file +# as your hardware file on the Config/radio/Hardware screen. + +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +import softrock +from softrock.hardware_usb import Hardware as BaseHardware + +class Hardware(BaseHardware): + def ChangeFrequency(self, tx_freq, vfo_freq, source='', band='', event=None): + vfo_freq = tx_freq - 10000 + tx, vfo = BaseHardware.ChangeFrequency(self, tx_freq, vfo_freq, source, band, event) + return tx, vfo diff --git a/sound.c b/sound.c new file mode 100644 index 0000000..9376152 --- /dev/null +++ b/sound.c @@ -0,0 +1,1675 @@ +/* + * Sound modules that do not depend on alsa or portaudio +*/ +#define PY_SSIZE_T_CLEAN +#include +#include +#include +#include +#include +#include + +#ifdef MS_WINDOWS +#include +#include +#else +#include +#include +#include + +#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#include +#if defined(BSD) +#include +#endif +#endif + +#endif + +#include "quisk.h" +#include "filter.h" + +#define DEBUG 0 + +// Thanks to Franco Spinelli for this fix: +// The H101 hardware using the PCM2904 chip has a one-sample delay between +// channels that must be fixed in software. If you have this problem, +// set channel_delay in your config file. The FIX_H101 #define is obsolete +// but still works. It is equivalent to channel_delay = channel_q. + +// The structure sound_dev represents a sound device to open. If portaudio_index +// is -1, it is an ALSA sound device; otherwise it is a portaudio device with that +// index. Portaudio devices have names that start with "portaudio". A device name +// can be the null string, meaning the device should not be opened. The sound_dev +// "handle" is either an alsa handle or a portaudio stream if the stream is open; +// otherwise it is NULL for a closed device. + +// Set DEBUG_MIC (in quisk.h) to send the microphone samples to the FFT instead of the radio samples. +// The sample rate and mic sample rate must be 48000. Use -c n2adr/conf4.py. +// 0: Normal operation. +// 1: Send filtered mic output to the FFT. +// 2: Send mic playback to the FFT and to the radio sound playback device "Playback". +// 3: Send unfiltered mic output to the FFT. + +#if DEBUG_IO +static int debug_timer = 1; // count up number of samples +#endif + +struct sound_dev quisk_Playback; +struct sound_dev quisk_MicPlayback; + +static struct sound_dev Capture, MicCapture, DigitalInput, DigitalOutput, RawSamplePlayback; +static struct sound_dev dr1, dr2, dr3, dr4, dr5, dr6, dr7, dr8, dr9; // sound output for digital sub-receivers +// These are arrays of all capture and playback devices, and MUST end with NULL: +static struct sound_dev * CaptureDevices[] = {&Capture, &MicCapture, &DigitalInput, NULL}; +struct sound_dev * quiskPlaybackDevices[QUISK_INDEX_SUB_RX1 + QUISK_MAX_SUB_RECEIVERS + 1] = { + &quisk_Playback, &quisk_MicPlayback, &DigitalOutput, &RawSamplePlayback, + &dr1, &dr2, &dr3, &dr4, &dr5, &dr6, &dr7, &dr8, &dr9, + NULL}; + +static SOCKET radio_sound_socket = INVALID_SOCKET; // send radio sound samples to a socket +static SOCKET radio_sound_mic_socket = INVALID_SOCKET; // receive mic samples from a socket +static int radio_sound_nshorts; // number of shorts (two bytes) to send +static int radio_sound_mic_nshorts; // number of shorts (two bytes) to receive +int quisk_active_sidetone; // quisk_active_sidetone == 0 No sidetone; == 1 sidetone from wasapi; == 2 sidetone from old logic +int quisk_midi_cwkey; // the CW key from MIDI NoteOn/NoteOff messages +int remote_control_head; +int remote_control_slave; + +struct sound_conf quisk_sound_state; // Current sound status +struct sound_conf * pt_quisk_sound_state = &quisk_sound_state; + +// Keep this array of names up to date with quisk.h +char * sound_format_names[4] = {"Int32", "Int16", "Float32", "Int24"} ; + +static struct wav_file file_rec_audio, file_rec_samples, file_rec_mic; +static int close_file_rec; + +static double digital_output_level = 0.7; +static int dc_remove_bw=100; // bandwidth of DC removal filter + +static ty_sample_start pt_sample_start; +static ty_sample_stop pt_sample_stop; +static ty_sample_read pt_sample_read; +ty_sample_write quisk_pt_sample_write; + +static complex double cSamples[SAMP_BUFFER_SIZE]; // Complex buffer for samples + +#if 0 +void quisk_sample_level(const char * msg, complex double * cSamples, int nSamples, double scale) +{ + static double time0 = 0; + static double level = 0; + static int count = 0; + double d; + int i; + + count += nSamples; + for (i = 0; i < nSamples; i++) { + d = cabs(cSamples[i]); + if (level < d) + level = d; + } + if (QuiskTimeSec() - time0 > 0.1) { + printf ("sample_level %s: %10.6lf count %8d\n", msg, level / scale, count); + level = 0; + count = 0; + time0 = QuiskTimeSec(); + } +} +#endif + +void ptimer(int counts) // used for debugging +{ // print the number of counts per second + static unsigned int calls=0, total=0; + static time_t time0=0; + time_t dt; + + if (time0 == 0) { + time0 = (int)(QuiskTimeSec() * 1.e6); + return; + } + total += counts; + calls++; + if (calls % 1000 == 0) { + dt = (int)(QuiskTimeSec() * 1.e6) - time0; + printf("ptimer: %d counts in %d microseconds %.3f counts/sec\n", + total, (unsigned)dt, (double)total * 1E6 / dt); + } +} + +static void delay_sample (struct sound_dev * dev, double * dSamp, int nSamples) +{ // Delay the I or Q data stream by one sample. + // cSamples is double D[nSamples][2] + double d; + double * first, * last; + + if (nSamples < 1) + return; + if (dev->channel_Delay == dev->channel_I) { + first = dSamp; + last = dSamp + nSamples * 2 - 2; + } + else if (dev->channel_Delay == dev->channel_Q) { + first = dSamp + 1; + last = dSamp + nSamples * 2 - 1; + } + else { + return; + } + d = dev->save_sample; + dev->save_sample = *last; + while (--nSamples) { + *last = *(last - 2); + last -= 2; + } + *first = d; +} + +static void correct_sample (struct sound_dev * dev, complex double * cSamples, int nSamples) +{ // Correct the amplitude and phase + int i; + double re, im; + + if (dev->doAmplPhase) { // amplitude and phase corrections + for (i = 0; i < nSamples; i++) { + re = creal(cSamples[i]); + im = cimag(cSamples[i]); + re = re * dev->AmPhAAAA; + im = re * dev->AmPhCCCC + im * dev->AmPhDDDD; + cSamples[i] = re + I * im; + } + } +} + +static void DCremove(complex double * cSamples, int nSamples, int sample_rate, int key_state) +{ + int i; + double omega, Qsin, Qcos, H0, x; + complex double c; + static int old_sample_rate = 0; + static int old_bandwidth = 0; + static double alpha = 0.95; + static complex double dc_remove = 0; + static complex double dc_average = 0; // Average DC component in samples + static complex double dc_sum = 0; + static int dc_count = 0; + static int dc_key_delay = 0; + + if (sample_rate != old_sample_rate || dc_remove_bw != old_bandwidth) { + old_sample_rate = sample_rate; // calculate a new alpha + old_bandwidth = dc_remove_bw; + if (old_bandwidth > 0) { + omega = M_PI * old_bandwidth / (old_sample_rate / 2.0); + Qsin = sin(omega); + Qcos = cos(omega); + H0 = 1.0 / sqrt(2.0); + x = ((Qcos - 1) * (Qcos - 1) + Qsin * Qsin) / (H0 * H0) - Qsin * Qsin; + x = sqrt(x); + alpha = Qcos - x; + //printf ("DC remove: alpha %.3f rate %i bw %i\n", alpha, old_sample_rate, old_bandwidth); + } + else { + //printf("DC remove: disable\n"); + } + } + if (quisk_is_vna || old_bandwidth == 0) { + } + else if (old_bandwidth == 1) { + if (key_state) { + dc_key_delay = 0; + dc_sum = 0; + dc_count = 0; + } + else if (dc_key_delay < old_sample_rate) { + dc_key_delay += nSamples; + } + else { + dc_count += nSamples; + for (i = 0; i < nSamples; i++) // Correction for DC offset in samples + dc_sum += cSamples[i]; + if (dc_count > old_sample_rate * 2) { + dc_average = dc_sum / dc_count; + //printf("dc average %lf %lf %d\n", creal(dc_average), cimag(dc_average), dc_count); + //printf("dc polar %.0lf %d\n", cabs(dc_average), + // (int)(360.0 / 2 / M_PI * atan2(cimag(dc_average), creal(dc_average)))); + dc_sum = 0; + dc_count = 0; + } + } + for (i = 0; i < nSamples; i++) // Correction for DC offset in samples + cSamples[i] -= dc_average; + } + else if (old_bandwidth > 1) { + for (i = 0; i < nSamples; i++) { // DC removal; R.G. Lyons page 553; 3rd Ed. p 762 + c = cSamples[i] + dc_remove * alpha; + cSamples[i] = c - dc_remove; + dc_remove = c; + } + } +} + +void quisk_record_audio(struct wav_file * wavfile, complex double * cSamples, int nSamples) +{ // Record the speaker audio to a WAV file, PCM, 16 bits, one channel + // TODO: correct for big-endian byte order + FILE * fp; + int j; + short samp; // must be 2 bytes + unsigned int u; // must be 4 bytes + unsigned short s; // must be 2 bytes + + switch (nSamples) { + case -1: // Open the file + if (wavfile->fp) + fclose(wavfile->fp); + wavfile->fp = fp = fopen(wavfile->file_name, "wb"); + if ( ! fp) { + return; + } + if (fwrite("RIFF", 1, 4, fp) != 4) { + fclose(fp); + wavfile->fp = NULL; + return; + } + // pcm data, 16-bit samples, one channel + u = 36; + fwrite(&u, 4, 1, fp); + fwrite("WAVE", 1, 4, fp); + fwrite("fmt ", 1, 4, fp); + u = 16; + fwrite(&u, 4, 1, fp); + s = 1; // wave_format_pcm + fwrite(&s, 2, 1, fp); + s = 1; // number of channels + fwrite(&s, 2, 1, fp); + u = quisk_Playback.sample_rate; // sample rate + fwrite(&u, 4, 1, fp); + u *= 2; + fwrite(&u, 4, 1, fp); + s = 2; + fwrite(&s, 2, 1, fp); + s = 16; + fwrite(&s, 2, 1, fp); + fwrite("data", 1, 4, fp); + u = 0; + fwrite(&u, 4, 1, fp); + wavfile->samples = 0; + break; + case -2: // close the file + if (wavfile->fp) + fclose(wavfile->fp); + wavfile->fp = NULL; + break; + default: // write the sound data to the file + fp = wavfile->fp; + if ( ! fp) + return; + u = (unsigned int)nSamples; + if (wavfile->samples >= 2147483629 - u) { // limit size to 2**32 - 1 + wavfile->samples = ~0; + u = ~0; + fseek(fp, 40, SEEK_SET); // seek from the beginning + fwrite(&u, 4, 1, fp); + fseek(fp, 4, SEEK_SET); + fwrite(&u, 4, 1, fp); + } + else { + wavfile->samples += u; + fseek(fp, 40, SEEK_SET); + u = 2 * wavfile->samples; + fwrite(&u, 4, 1, fp); + fseek(fp, 4, SEEK_SET); + u += 36; + fwrite(&u, 4, 1, fp); + } + fseek(fp, 0, SEEK_END); // seek to the end + for (j = 0; j < nSamples; j++) { + samp = (short)(creal(cSamples[j]) / 65536.0); + fwrite(&samp, 2, 1, fp); + } + break; + } +} + +static int record_samples(struct wav_file * wavfile, complex double * cSamples, int nSamples) +{ // Record the samples to a WAV file, two float samples I/Q + FILE * fp; // TODO: correct for big-endian byte order + int j; + float samp; // must be 4 bytes + unsigned int u; // must be 4 bytes + unsigned short s; // must be 2 bytes + + switch (nSamples) { + case -1: // Open the file + if (wavfile->fp) + fclose(wavfile->fp); + wavfile->fp = fp = fopen(wavfile->file_name, "wb"); + if ( ! fp) { + return 0; + } + if (fwrite("RIFF", 1, 4, fp) != 4) { + fclose(fp); + wavfile->fp = NULL; + return 0; + } + // IEEE float data, two channels + u = 36; + fwrite(&u, 4, 1, fp); + fwrite("WAVE", 1, 4, fp); + fwrite("fmt ", 1, 4, fp); + u = 16; + fwrite(&u, 4, 1, fp); + s = 3; // wave_format_ieee_float + fwrite(&s, 2, 1, fp); + s = 2; // number of channels + fwrite(&s, 2, 1, fp); + u = quisk_sound_state.sample_rate; // sample rate + fwrite(&u, 4, 1, fp); + u *= 8; + fwrite(&u, 4, 1, fp); + s = 8; + fwrite(&s, 2, 1, fp); + s = 32; + fwrite(&s, 2, 1, fp); +// Add a LIST chunk of type INFO for further metadata + fwrite("data", 1, 4, fp); + u = 0; + fwrite(&u, 4, 1, fp); + wavfile->samples = 0; + break; + case -2: // close the file + if (wavfile->fp) + fclose(wavfile->fp); + wavfile->fp = NULL; + break; + default: // write the sound data to the file + fp = wavfile->fp; + if ( ! fp) + return 0; + u = (unsigned int)nSamples; + if (wavfile->samples >= 536870907 - u) { // limit size to 2**32 - 1 + wavfile->samples = ~0; + u = ~0; + fseek(fp, 40, SEEK_SET); // seek from the beginning + fwrite(&u, 4, 1, fp); + fseek(fp, 4, SEEK_SET); // seek from the beginning + fwrite(&u, 4, 1, fp); + } + else { + wavfile->samples += u; + fseek(fp, 40, SEEK_SET); // seek from the beginning + u = 8 * wavfile->samples; + fwrite(&u, 4, 1, fp); + fseek(fp, 4, SEEK_SET); // seek from the beginning + u += 36 ; + fwrite(&u, 4, 1, fp); + } + fseek(fp, 0, SEEK_END); // seek to the end + for (j = 0; j < nSamples; j++) { + samp = creal(cSamples[j]) / CLIP32; + fwrite(&samp, 4, 1, fp); + samp = cimag(cSamples[j]) / CLIP32; + fwrite(&samp, 4, 1, fp); + } + break; + } + return 1; +} + +void quisk_sample_source(ty_sample_start start, ty_sample_stop stop, ty_sample_read read) +{ + pt_sample_start = start; + pt_sample_stop = stop; + pt_sample_read = read; +} + +void quisk_sample_source4(ty_sample_start start, ty_sample_stop stop, ty_sample_read read, ty_sample_write write) +{ + pt_sample_start = start; + pt_sample_stop = stop; + pt_sample_read = read; + quisk_pt_sample_write = write; +} + +/*! + * \brief Driver interface for reading samples from a device + * + * \param dev Input. Device to read from + * \param cSamples Output. Read samples. + * \returns number of samples read + */ +int read_sound_interface( + struct sound_dev* dev, + complex double * cSamples +) +{ + int i, nSamples; + double avg, samp, re, im, frac, diff; + + // Read using correct driver. + switch( dev->driver ) + { + case DEV_DRIVER_DIRECTX: + nSamples = quisk_read_directx(dev, cSamples); + break; + case DEV_DRIVER_WASAPI: + nSamples = quisk_read_wasapi(dev, cSamples); + break; + case DEV_DRIVER_PORTAUDIO: + nSamples = quisk_read_portaudio(dev, cSamples); + break; + case DEV_DRIVER_ALSA: + nSamples = quisk_read_alsa(dev, cSamples); + break; + case DEV_DRIVER_PULSEAUDIO: + nSamples = quisk_read_pulseaudio(dev, cSamples); + break; + case DEV_DRIVER_NONE: + default: + return 0; + } + if ( ! cSamples || nSamples <= 0 || dev->sample_rate <= 0) // cSamples can be NULL + return nSamples; + // Calculate average squared level + avg = dev->average_square; + frac = 1.0 / (0.2 * dev->sample_rate); + for (i = 0; i < nSamples; i++) { + re = creal(cSamples[i]); + im = cimag(cSamples[i]); + samp = re * re + im * im; + diff = samp - avg; + if (diff >= 0) + avg = samp; // set to peak value + else + avg = avg + frac * diff; + } + dev->average_square = avg; + return nSamples; +} + +/*! + * \brief Driver interface for playing samples to a device + * + * \param dev Input. Device to play to + * \param nSamples Input. Number of samples to play + * \param cSamples Input. Samples to play + * \param report_latency Input. 1 to report latency, 0 otherwise. + * \param volume Input. [0,1] volume ratio + * \returns number of samples read + */ +#define AVG_SEC 10.0 +void play_sound_interface(struct sound_dev* dev, int nSamples, complex double * cSamples, int report_latency, double volume) +{ + int i; + double avg, samp, re, im, frac, diff, tm; + + if (cSamples && nSamples > 0 && dev->sample_rate > 0) { + // Calculate average squared level + avg = dev->average_square; + frac = 1.0 / (0.2 * dev->sample_rate); + for (i = 0; i < nSamples; i++) { + re = creal(cSamples[i]); + im = cimag(cSamples[i]); + samp = re * re + im * im; + diff = samp - avg; + if (diff >= 0) + avg = samp; // set to peak value + else + avg = avg + frac * diff; + } + dev->average_square = avg; + } +#if 0 + static int disturb_time = 0; + // Damage the sample rate for debug + disturb_time += nSamples; + if (disturb_time > 300) { + nSamples++; + disturb_time = 0; + } +#endif + // Correct sample rate by reference to the buffer_fill <> 0.5 + if (dev->cr_correction != 0) { + dev->cr_sample_time += nSamples; + if (dev->cr_sample_time >= dev->cr_correct_time && nSamples >= 2) { + dev->cr_sample_time = 0; + if (dev->cr_correction > 0) { // add a sample + //printf("Add a sample\n"); + cSamples[nSamples] = cSamples[nSamples - 1]; + cSamples[nSamples - 1] = (cSamples[nSamples - 2] + cSamples[nSamples]) / 2.0; + nSamples++; + } + else { // remove a sample + //printf("Remove a sample\n"); + nSamples--; + } + } + } + // Play using correct driver. + switch( dev->driver ) { + case DEV_DRIVER_DIRECTX: + quisk_play_directx(dev, nSamples, cSamples, report_latency, volume); + break; + case DEV_DRIVER_WASAPI: + quisk_play_wasapi(dev, nSamples, cSamples, volume); + break; + case DEV_DRIVER_WASAPI2: + quisk_write_wasapi(dev, nSamples, cSamples, volume); + break; + case DEV_DRIVER_PORTAUDIO: + quisk_play_portaudio(dev, nSamples, cSamples, report_latency, volume); + break; + case DEV_DRIVER_ALSA: + quisk_play_alsa(dev, nSamples, cSamples, report_latency, volume); + break; + case DEV_DRIVER_PULSEAUDIO: + quisk_play_pulseaudio(dev, nSamples, cSamples, report_latency, volume); + break; + case DEV_DRIVER_NONE: + default: + break; + } + // Calculate a new sample rate correction + tm = QuiskTimeSec(); + if (tm - dev->TimerTime0 > AVG_SEC) { + dev->TimerTime0 = tm; + if (dev->cr_average_count <= 0) { + dev->cr_correction = 0; + } + else if (dev->dev_index == t_MicPlayback && (rxMode == CWL || rxMode == CWU)) { + dev->cr_correction = 0; + dev->cr_average_fill /= dev->cr_average_count; + if (quisk_sound_state.verbose_sound > 1) + QuiskPrintf("%s: Buffer average %5.2lf\n", dev->stream_description, dev->cr_average_fill * 100); + } + else if (dev->cr_delay > 0) { + dev->cr_delay--; + dev->cr_correction = 0; + dev->cr_average_fill /= dev->cr_average_count; + if (quisk_sound_state.verbose_sound > 1) + QuiskPrintf("%s: Buffer average %5.2lf\n", dev->stream_description, dev->cr_average_fill * 100); + } + else if ( ! (dev->dev_index == t_Playback || dev->dev_index == t_MicPlayback)) { + dev->cr_correction = 0; + dev->cr_average_fill /= dev->cr_average_count; + if (quisk_sound_state.verbose_sound > 1) + QuiskPrintf("%s: Buffer average %5.2lf\n", dev->stream_description, dev->cr_average_fill * 100); + } + else { + dev->cr_average_fill /= dev->cr_average_count; + if (dev->cr_average_fill > 0.55) + dev->cr_correction = -0.05 * dev->play_buf_size; + else if (dev->cr_average_fill < 0.45) + dev->cr_correction = 0.05 * dev->play_buf_size; + else + dev->cr_correction = (0.5 - dev->cr_average_fill) * dev->play_buf_size; + if (dev->cr_correction != 0) + dev->cr_correct_time = abs(dev->sample_rate * AVG_SEC / dev->cr_correction); + if (quisk_sound_state.verbose_sound > 1) + QuiskPrintf("%s: Buffer average %5.2lf cr_correction %5d\n", + dev->stream_description, dev->cr_average_fill * 100, dev->cr_correction); + } + dev->cr_average_fill = 0; + dev->cr_average_count = 0; + dev->cr_sample_time = 0; + } +} + +static int read_radio_sound_socket(complex double * cSamples) +{ + int i, bytes, nSamples; + short s; + double d; + struct timeval tm_wait; + char buf[1500]; + fd_set fds; + static int started = 0; + + nSamples = 0; + while (1) { // read all available blocks + if (nSamples > SAMP_BUFFER_SIZE / 2) + break; + tm_wait.tv_sec = 0; + tm_wait.tv_usec = 0; + FD_ZERO (&fds); + FD_SET (radio_sound_mic_socket, &fds); + if (select (radio_sound_mic_socket + 1, &fds, NULL, NULL, &tm_wait) != 1) + break; + bytes = recv(radio_sound_mic_socket, buf, 1500, 0); + if (bytes == radio_sound_mic_nshorts * 2) { // required block size + started = 1; + for (i = 2; i < bytes; i += 2) { + memcpy(&s, buf + i, 2); + d = (double)s / CLIP16 * CLIP32; // convert 16-bit samples to 32 bits + cSamples[nSamples++] = d + I * d; + } + } + } + if ( ! started && nSamples == 0) { + i = send(radio_sound_mic_socket, "rr", 2, 0); + if (i != 2) + printf("read_radio_sound_mic_socket returned %d\n", i); + } + return nSamples; +} + +static void send_radio_sound_socket(complex double * cSamples, int count, double volume) +{ // Send count samples. Each sample is sent as two shorts (4 bytes) of I/Q data. + // Send an initial two bytes of zero for each block. + // Transmission is delayed until a whole block of data is available. + int i, sent; + static short udp_iq[750] = {0}; // Documented maximum radio sound samples is 367 + static int udp_size = 1; + + for (i = 0; i < count; i++) { + udp_iq[udp_size++] = (short)(creal(cSamples[i]) * volume * (double)CLIP16 / CLIP32); + udp_iq[udp_size++] = (short)(cimag(cSamples[i]) * volume * (double)CLIP16 / CLIP32); + if (udp_size >= radio_sound_nshorts) { // check count + sent = send(radio_sound_socket, (char *)udp_iq, udp_size * 2, 0); + if (sent != udp_size * 2) + printf("Send audio socket returned %d\n", sent); + udp_size = 1; + } + } +} + +void * quisk_make_txIQ(struct sound_dev * dev, int rewind) +{ // make complex samples to transmit CW on SoundCard + static float f32[2]; + static int16_t i16[2]; + static int32_t i32[2]; + static int8_t i24[6]; + static float envelopeVol=0; + int32_t n; + float rise_time; + float d, dSamp0, dSamp1; + static complex double tuneVector = 1.0; + complex double sample, tx_mic_phase; + + rise_time = 0.707 / (dev->sample_rate * 4.0 / 1000.0); // milliseconds rise time + if (QUISK_CWKEY_DOWN) { + if (envelopeVol < 0.707) { + envelopeVol += rise_time; + if (envelopeVol > 0.707) + envelopeVol = 0.707; + } + } + else { + if (envelopeVol > 0.0) { + envelopeVol -= rise_time; + if (envelopeVol < 0.0) + envelopeVol = 0.0; + } + } + if (rewind) { // rewind the phase + tx_mic_phase = cexp(( -I * 2.0 * M_PI * quisk_tx_tune_freq) / dev->sample_rate); + tuneVector /= cpow(tx_mic_phase, rewind); + return NULL; + } + if (envelopeVol > 0) { + tx_mic_phase = cexp(( -I * 2.0 * M_PI * quisk_tx_tune_freq) / dev->sample_rate); + sample = envelopeVol * tuneVector * quisk_sound_state.mic_out_volume; + dSamp0 = creal(sample); + dSamp1 = cimag(sample); + tuneVector *= tx_mic_phase; + } + else { + i32[0] = i32[1] = 0; + return i32; + } + // maybe delay the I or Q channel by one sample + if (dev->channel_Delay == dev->channel_I) { + d = dev->save_sample; + dev->save_sample = dSamp0; + dSamp0 = d; + } + else if (dev->channel_Delay == dev->channel_Q) { + d = dev->save_sample; + dev->save_sample = dSamp1; + dSamp1 = d; + } + // amplitude and phase corrections + if (dev->doAmplPhase) { + dSamp0 = dSamp0 * dev->AmPhAAAA; + dSamp1 = dSamp0 * dev->AmPhCCCC + dSamp1 * dev->AmPhDDDD; + } + switch (dev->sound_format) { + case Int16: + i16[0] = (int16_t)(dSamp0 * CLIP16); + i16[1] = (int16_t)(dSamp1 * CLIP16); + return i16; + case Int24: // only works for little-endian + n = (int32_t)(dSamp0 * CLIP32); + memcpy(i24, (int8_t *)&n + 1, 3); + n = (int32_t)(dSamp1 * CLIP32); + memcpy(i24 + 3, (int8_t *)&n + 1, 3); + return i24; + case Int32: + default: + i32[0] = (int32_t)(dSamp0 * CLIP32); + i32[1] = (int32_t)(dSamp1 * CLIP32); + return i32; + case Float32: + f32[0] = dSamp0; + f32[1] = dSamp1; + return f32; + } +} + +void * quisk_make_sidetone(struct sound_dev * dev, int rewind) +{ // make sidetone samples + static float f32; + static int16_t i16; + static int32_t i32; + static float envelopeVol=0; + static float phase = 0.0; + float rise_time; + + if (rewind) { // rewind the phase + phase -= fmodf(2.0 * M_PI * quisk_sidetoneFreq / dev->sample_rate * rewind, 2.0 * M_PI); + return NULL; + } + rise_time = 0.707 / (dev->sample_rate * 4.0 / 1000.0); // milliseconds rise time + if (QUISK_CWKEY_DOWN) { + if (envelopeVol < 0.707) { + envelopeVol += rise_time; + if (envelopeVol > 0.707) + envelopeVol = 0.707; + } + } + else { + if (envelopeVol > 0.0) { + envelopeVol -= rise_time; + if (envelopeVol < 0.0) + envelopeVol = 0.0; + } + } + if (phase < 0) + phase += 2.0 * M_PI; + else if (phase > 2.0 * M_PI) + phase -= 2.0 * M_PI; + if (envelopeVol > 0) { + phase += 2.0 * M_PI * quisk_sidetoneFreq / dev->sample_rate; + f32 = sinf(phase) * envelopeVol * quisk_sidetoneVolume; + } + else { + i32 = 0; + return &i32; + } + switch (dev->sound_format) { + case Int16: + i16 = (int16_t)(f32 * CLIP16); + return &i16; + case Int24: // only works for little-endian + i32 = (int32_t)(f32 * CLIP32); + return (unsigned char *)&i32 + 1; + case Int32: + default: + i32 = (int32_t)(f32 * CLIP32); + return &i32; + case Float32: + return &f32; + } +} + +int quisk_play_sidetone(struct sound_dev * dev) +{ // return 1 if we played the ALSA or PULSEAUDIO sidetone + static int last_play_state = 0; + + if (quisk_play_state <= RECEIVE && last_play_state <= RECEIVE) + return 0; + last_play_state = quisk_play_state; + if (quisk_isFDX) + return 0; +#ifdef QUISK_HAVE_ALSA + if (quisk_active_sidetone == 3 && dev->driver == DEV_DRIVER_ALSA) { + quisk_alsa_sidetone(dev); + return 1; + } +#endif +#ifdef QUISK_HAVE_PULSEAUDIO + if (quisk_active_sidetone == 4 && dev->driver == DEV_DRIVER_PULSEAUDIO) { + quisk_pulseaudio_sidetone(dev); + return 1; + } +#endif + return 0; +} + +int quisk_read_sound(void) // Called from sound thread +{ // called in an infinite loop by the main program + int i, nSamples, mic_count, mic_interp, retval, is_cw, mic_sample_rate; + double mic_play_volume; + complex double tx_mic_phase; + static double cwEnvelope=0; + static double cwCount=0; + static complex double tuneVector = (double)CLIP32 / CLIP16; // Convert 16-bit to 32-bit samples + static struct quisk_cFilter filtInterp={NULL}; + int key_state, is_DGT; +#if DEBUG_IO > 1 + char str80[80]; // Extra debug output by Ben Cahill, AC2YD +#endif +#if DEBUG_MIC == 1 + complex double tmpSamples[SAMP_BUFFER_SIZE]; +#endif + + if (close_file_rec) { + close_file_rec = 0; + quisk_record_audio(&file_rec_audio, NULL, -2); + quisk_record_audio(&file_rec_mic, NULL, -2); + record_samples(&file_rec_samples, NULL, -2); + } + //QuiskPrintTime("quisk_read_sound start", 0); + quisk_sound_state.interrupts++; + if (quisk_use_serial_port) { + quisk_poll_hardware_key(); + } + quisk_set_play_state(); + key_state = quisk_is_key_down(); //reading this once is important for predicable bevavior on cork/flush +#if DEBUG_IO > 1 + QuiskPrintTime("Start quisk_read_sound", 0); +#endif + +#ifdef QUISK_HAVE_PULSEAUDIO + if (quisk_sound_state.IQ_server[0] && ! (rxMode == CWL || rxMode == CWU)) { + if (Capture.handle && Capture.driver == DEV_DRIVER_PULSEAUDIO) { + if (key_state == 1 && !Capture.cork_status) + quisk_cork_pulseaudio(&Capture, 1); + else if (key_state == 0 && Capture.cork_status) { + quisk_cork_pulseaudio(&Capture, 0); + quisk_flush_pulseaudio(&Capture); + } + } + if (quisk_MicPlayback.handle && quisk_MicPlayback.driver == DEV_DRIVER_PULSEAUDIO) { + if (key_state == 0 && !quisk_MicPlayback.cork_status) + quisk_cork_pulseaudio(&quisk_MicPlayback, 1); + else if (key_state == 1 && quisk_MicPlayback.cork_status) { + quisk_cork_pulseaudio(&quisk_MicPlayback, 0); + quisk_flush_pulseaudio(&quisk_MicPlayback); + } + } + } + else if (quisk_sound_state.IQ_server[0]) { + if (Capture.handle && Capture.driver == DEV_DRIVER_PULSEAUDIO) { + if (Capture.cork_status) + quisk_cork_pulseaudio(&Capture, 0); + } + if (quisk_MicPlayback.handle && quisk_MicPlayback.driver == DEV_DRIVER_PULSEAUDIO) { + if (quisk_MicPlayback.cork_status) + quisk_cork_pulseaudio(&quisk_MicPlayback, 0); + } + } +#endif + + if (pt_sample_read) { // read samples from SDR-IQ or UDP or SoapySDR + nSamples = (*pt_sample_read)(cSamples); + DCremove(cSamples, nSamples, quisk_sound_state.sample_rate, key_state); + if (nSamples <= 0) + QuiskSleepMicrosec(2000); + } + else if (Capture.handle) { // blocking read from soundcard + //QuiskPrintTime("quisk_read_sound start", 0); + nSamples = read_sound_interface(&Capture, cSamples); + //QuiskPrintTime("quisk_read_sound end", 0); + if (Capture.channel_Delay >= 0) // delay the I or Q channel by one sample + delay_sample(&Capture, (double *)cSamples, nSamples); + if (Capture.doAmplPhase) // amplitude and phase corrections + correct_sample(&Capture, cSamples, nSamples); + DCremove(cSamples, nSamples, quisk_sound_state.sample_rate, key_state); + if (nSamples <= 0) + QuiskSleepMicrosec(2000); +#if DEBUG_IO > 1 + snprintf(str80, 80, " rx i/q read %d samples", nSamples); + QuiskPrintTime(str80, 0); +#endif + } + else { + QuiskSleepMicrosec(5000); + nSamples = (int)(0.5 + QuiskDeltaSec(1) * quisk_sound_state.sample_rate); + if (nSamples > SAMP_BUFFER_SIZE / 2) + nSamples = SAMP_BUFFER_SIZE / 2; + for (i = 0; i < nSamples; i++) + cSamples[i] = 0; + } + //QuiskPrintTime("quisk_read_sound end", 0); + retval = nSamples; // retval remains the number of samples read +#if DEBUG_IO + debug_timer += nSamples; + if (debug_timer >= quisk_sound_state.sample_rate) // one second + debug_timer = 0; +#endif +#if DEBUG_IO > 2 + ptimer (nSamples); +#endif + quisk_sound_state.latencyCapt = nSamples; // samples available + // Perhaps record the Rx samples to a file + if ( ! key_state && file_rec_samples.fp) + record_samples(&file_rec_samples, cSamples, nSamples); + // Perhaps write samples to a loopback device for use by another program + if (RawSamplePlayback.handle) + play_sound_interface(&RawSamplePlayback, nSamples, cSamples, 0, 1.0); + // Perhaps replace the samples with samples from a file + if (quisk_record_state == FILE_PLAY_SAMPLES) + quisk_play_samples(cSamples, nSamples); +#if ! DEBUG_MIC + nSamples = quisk_process_samples(cSamples, nSamples); +#endif +#if DEBUG_IO > 1 + snprintf(str80, 80, " rx i/q process %d samples", nSamples); + QuiskPrintTime(str80, 0); +#endif + // For control_head role in remote control operation, receive radio sound via UDP + if (remote_control_head) + nSamples = read_remote_radio_sound_socket(cSamples); + // For remote_radio role in remote control operation, send radio sound via UDP + if (remote_control_slave) + send_remote_radio_sound_socket(cSamples, nSamples); + + is_DGT = rxMode == DGT_U || rxMode == DGT_L || rxMode == DGT_IQ || rxMode == DGT_FM; + if (quisk_record_state == TMP_PLAY_SPKR_MIC) + quisk_tmp_playback(cSamples, nSamples, 1.0); // replace radio sound + else if (quisk_record_state == FILE_PLAY_SPKR_MIC) + quisk_file_playback(cSamples, nSamples, 1.0); // replace radio sound + // Play the demodulated audio +#if DEBUG_MIC != 2 + if ( ! quisk_play_sidetone(&quisk_Playback)) // play sidetone + play_sound_interface(&quisk_Playback, nSamples, cSamples, 1, quisk_audioVolume); // else play radio sound +#endif + if (radio_sound_socket != INVALID_SOCKET) + send_radio_sound_socket(cSamples, nSamples, quisk_audioVolume); + + // Play digital if required + if (is_DGT) + play_sound_interface(&DigitalOutput, nSamples, cSamples, 1, digital_output_level); + + // Perhaps record the speaker audio to a file + if ( ! key_state && file_rec_audio.fp) + quisk_record_audio(&file_rec_audio, cSamples, nSamples); // Record Rx samples + +#if DEBUG_IO > 1 + snprintf(str80, 80, " play %d radio sound samples", nSamples); + QuiskPrintTime(str80, 0); +#endif + // Read and process the microphone + mic_sample_rate = quisk_sound_state.mic_sample_rate; + if (MicCapture.handle) { + mic_count = read_sound_interface(&MicCapture, cSamples); +#if DEBUG_IO > 1 + snprintf(str80, 80, " mic read %d samples", mic_count); + QuiskPrintTime(str80, 0); +#endif + } + else if (radio_sound_mic_socket != INVALID_SOCKET) + mic_count = read_radio_sound_socket(cSamples); + else { // No mic source; use zero samples + mic_count = (int)(0.5 + QuiskDeltaSec(0) * mic_sample_rate); + if (mic_count > SAMP_BUFFER_SIZE / 2) + mic_count = SAMP_BUFFER_SIZE / 2; + for (i = 0; i < mic_count; i++) + cSamples[i] = 0; + } + //QuiskPrintTime("quisk_read_sound end", 0); + if (quisk_record_state == TMP_PLAY_SPKR_MIC) // Discard previous samples and replace with saved sound + quisk_tmp_microphone(cSamples, mic_count); + else if (quisk_record_state == FILE_PLAY_SPKR_MIC) // Discard previous samples and replace with saved sound + quisk_file_microphone(cSamples, mic_count); + if (DigitalInput.handle) { + if (is_DGT) { // Discard previous mic samples and use digital samples + mic_sample_rate = DigitalInput.sample_rate; + mic_count = read_sound_interface(&DigitalInput, cSamples); + } + else { // Read and discard any digital samples + read_sound_interface(&DigitalInput, NULL); + } + } + else if (is_DGT) { // Use zero-valued samples + for (i = 0; i < mic_count; i++) + cSamples[i] = 0; + } + //quisk_sample_level("read mic or DGT", cSamples, mic_count, CLIP16); + // Perhaps record the microphone audio to the speaker audio file + if (key_state && file_rec_audio.fp) + quisk_record_audio(&file_rec_audio, cSamples, mic_count); + // Perhaps record the microphone audio to the microphone audio file + if (file_rec_mic.fp) + quisk_record_audio(&file_rec_mic, cSamples, mic_count); + + // For remote_radio role in remote control operation, read mic sound via UDP; replace any mic sound from above. + if (remote_control_slave) + mic_count = read_remote_mic_sound_socket(cSamples); + + // For control_head role in remote control operation, send mic sound via UDP + if (remote_control_head) + send_remote_mic_sound_socket(cSamples, mic_count); + + if (mic_count > 0) { +#if DEBUG_MIC == 3 + quisk_process_samples(cSamples, mic_count); +#endif + // quisk_process_microphone returns samples at the sample rate MIC_OUT_RATE + mic_count = quisk_process_microphone(mic_sample_rate, cSamples, mic_count); +#if DEBUG_MIC == 1 + for (i = 0; i < mic_count; i++) + tmpSamples[i] = cSamples[i] * (double)CLIP32 / CLIP16; // convert 16-bit samples to 32 bits + quisk_process_samples(tmpSamples, mic_count); +#endif +#if DEBUG_IO > 1 + snprintf(str80, 80, " mic process %d samples", mic_count); + QuiskPrintTime(str80, 0); +#endif + } + //quisk_sample_level("quisk_process_microphone", cSamples, mic_count, CLIP16); + // Mic playback without a mic is needed for CW + if (quisk_MicPlayback.handle) { // Mic playback: send mic I/Q samples to a sound card + //quisk_sample_level("quisk_MicPlayback.handle", cSamples, mic_count, CLIP16); + mic_play_volume = 1.0; + if (rxMode == CWL || rxMode == CWU) { // Transmit CW + is_cw = 1; + } + else { + is_cw = 0; + cwCount = 0; + cwEnvelope = 0.0; + } + tx_mic_phase = cexp(( -I * 2.0 * M_PI * quisk_tx_tune_freq) / quisk_MicPlayback.sample_rate); + if (is_cw) { // Transmit CW; use capture device for timing, not microphone + cwCount += (double)retval * quisk_MicPlayback.sample_rate / quisk_sound_state.sample_rate; + mic_count = 0; + if (QUISK_CWKEY_DOWN || quiskSpotLevel >= 0) { + while (cwCount >= 1.0) { + if (cwEnvelope < 1.0) { + cwEnvelope += 1. / (quisk_MicPlayback.sample_rate * 5e-3); // 5 milliseconds + if (cwEnvelope > 1.0) + cwEnvelope = 1.0; + } + if (quiskSpotLevel >= 0) + cSamples[mic_count++] = (CLIP16 - 1) * cwEnvelope * quiskSpotLevel / 1000.0 * tuneVector * quisk_sound_state.mic_out_volume; + else + cSamples[mic_count++] = (CLIP16 - 1) * cwEnvelope * tuneVector * quisk_sound_state.mic_out_volume; + tuneVector *= tx_mic_phase; + cwCount -= 1; + } + } + else { // key is up + while (cwCount >= 1.0) { + if (cwEnvelope > 0.0) { + cwEnvelope -= 1.0 / (quisk_MicPlayback.sample_rate * 5e-3); // 5 milliseconds + if (cwEnvelope < 0.0) + cwEnvelope = 0.0; + } + if (quiskSpotLevel >= 0) + cSamples[mic_count++] = (CLIP16 - 1) * cwEnvelope * quiskSpotLevel / 1000.0 * tuneVector * quisk_sound_state.mic_out_volume; + else + cSamples[mic_count++] = (CLIP16 - 1) * cwEnvelope * tuneVector * quisk_sound_state.mic_out_volume; + tuneVector *= tx_mic_phase; + cwCount -= 1; + } + } + } + else if( ! DEBUG_MIC && ! quisk_is_key_down()) { // Not CW and key up: zero samples + mic_play_volume = 0.0; + for (i = 0; i < mic_count; i++) + cSamples[i] = 0.0; + } + // Perhaps interpolate the mic samples back to the mic play rate + mic_interp = quisk_MicPlayback.sample_rate / MIC_OUT_RATE; + if ( ! is_cw && mic_interp > 1) { + if (! filtInterp.dCoefs) + quisk_filt_cInit(&filtInterp, quiskFilt12_19Coefs, sizeof(quiskFilt12_19Coefs)/sizeof(double)); + mic_count = quisk_cInterpolate(cSamples, mic_count, &filtInterp, mic_interp); + } + // Tune the samples to frequency and convert 16-bit samples to 32-bits (using tuneVector) + if ( ! is_cw) { + for (i = 0; i < mic_count; i++) { + cSamples[i] = conj(cSamples[i]) * tuneVector * quisk_sound_state.mic_out_volume; + tuneVector *= tx_mic_phase; + } + } + // Correct order of delay and phase by Chuck Ritola. Thanks Chuck. + // amplitude and phase corrections + if (quisk_MicPlayback.doAmplPhase) + correct_sample (&quisk_MicPlayback, cSamples, mic_count); + // delay the I or Q channel by one sample + if (quisk_MicPlayback.channel_Delay >= 0) + delay_sample(&quisk_MicPlayback, (double *)cSamples, mic_count); + // play mic samples + //quisk_sample_level("play quisk_MicPlayback", cSamples, mic_count, CLIP32); + play_sound_interface(&quisk_MicPlayback, mic_count, cSamples, 1, mic_play_volume); +#if DEBUG_MIC == 2 + play_sound_interface(&Playback, mic_count, cSamples, 1, quisk_audioVolume); + quisk_process_samples(cSamples, mic_count); +#endif + } +#if DEBUG_IO > 1 + snprintf(str80, 80, " play %d tx i/q samples; finished", mic_count); + QuiskPrintTime(str80, 0); +#endif + // Return negative number for error + return retval; +} + +int quisk_get_overrange(void) // Called from GUI thread +{ // Return the overrange (ADC clip) counter, then zero it + int i; + + i = quisk_sound_state.overrange + Capture.overrange; + quisk_sound_state.overrange = 0; + Capture.overrange = 0; + return i; +} + +void quisk_close_sound(void) // Called from sound thread +{ + quisk_active_sidetone = 0; // No sidetone +#ifdef MS_WINDOWS + int cleanup = radio_sound_socket != INVALID_SOCKET || radio_sound_mic_socket != INVALID_SOCKET; +#endif + quisk_close_sound_directx(CaptureDevices, quiskPlaybackDevices); + quisk_close_sound_wasapi(CaptureDevices, quiskPlaybackDevices); + quisk_close_sound_portaudio(); + quisk_close_sound_alsa(CaptureDevices, quiskPlaybackDevices); + quisk_close_sound_pulseaudio(); + if (pt_sample_stop) + (*pt_sample_stop)(); + strMcpy (quisk_sound_state.err_msg, CLOSED_TEXT, QUISK_SC_SIZE); + if (radio_sound_socket != INVALID_SOCKET) { + close(radio_sound_socket); + radio_sound_socket = INVALID_SOCKET; + } + if (radio_sound_mic_socket != INVALID_SOCKET) { + shutdown(radio_sound_mic_socket, QUISK_SHUT_RD); + send(radio_sound_mic_socket, "ss", 2, 0); + send(radio_sound_mic_socket, "ss", 2, 0); + QuiskSleepMicrosec(1000000); + close(radio_sound_mic_socket); + radio_sound_mic_socket = INVALID_SOCKET; + } + quisk_play_state = SHUTDOWN; +#ifdef MS_WINDOWS + if (cleanup) + WSACleanup(); +#endif +} + +static void set_num_channels(struct sound_dev * dev) +{ // Set num_channels to the maximum channel index plus one + dev->num_channels = dev->channel_I; + if (dev->num_channels < dev->channel_Q) + dev->num_channels = dev->channel_Q; + dev->num_channels++; +} + +static void open_radio_sound_socket(void) +{ + struct sockaddr_in Addr; + int samples, port, sndsize = 48000; + char radio_sound_ip[QUISK_SC_SIZE]; + char radio_sound_mic_ip[QUISK_SC_SIZE]; +#ifdef MS_WINDOWS + WORD wVersionRequested; + WSADATA wsaData; +#endif + + dc_remove_bw = QuiskGetConfigInt ("dc_remove_bw", 100); + strMcpy(radio_sound_ip, QuiskGetConfigString ("radio_sound_ip", ""), QUISK_SC_SIZE); + strMcpy(radio_sound_mic_ip, QuiskGetConfigString ("radio_sound_mic_ip", ""), QUISK_SC_SIZE); + if (radio_sound_ip[0] == 0 && radio_sound_mic_ip[0] == 0) + return; +#ifdef MS_WINDOWS + wVersionRequested = MAKEWORD(2, 2); + if (WSAStartup(wVersionRequested, &wsaData) != 0) { + printf("open_radio_sound_socket: Failure to start WinSock\n"); + return; // failure to start winsock + } +#endif + if (radio_sound_ip[0]) { + port = QuiskGetConfigInt ("radio_sound_port", 0); + samples = QuiskGetConfigInt ("radio_sound_nsamples", 360); + if (samples > 367) + samples = 367; + radio_sound_nshorts = samples * 2 + 1; + radio_sound_socket = socket(PF_INET, SOCK_DGRAM, 0); + if (radio_sound_socket != INVALID_SOCKET) { + setsockopt(radio_sound_socket, SOL_SOCKET, SO_SNDBUF, (char *)&sndsize, sizeof(sndsize)); + Addr.sin_family = AF_INET; + Addr.sin_port = htons(port); +#ifdef MS_WINDOWS + Addr.sin_addr.S_un.S_addr = inet_addr(radio_sound_ip); +#else + inet_aton(radio_sound_ip, &Addr.sin_addr); +#endif + if (connect(radio_sound_socket, (const struct sockaddr *)&Addr, sizeof(Addr)) != 0) { + close(radio_sound_socket); + radio_sound_socket = INVALID_SOCKET; + } + } + if (radio_sound_socket == INVALID_SOCKET) { + printf("open_radio_sound_socket: Failure to open socket\n"); + } + else { +#if DEBUG_IO + printf("open_radio_sound_socket: opened socket %s\n", radio_sound_ip); +#endif + } + } + if (radio_sound_mic_ip[0]) { + port = QuiskGetConfigInt ("radio_sound_mic_port", 0); + samples = QuiskGetConfigInt ("radio_sound_mic_nsamples", 720); + if (samples > 734) + samples = 734; + radio_sound_mic_nshorts = samples + 1; + radio_sound_mic_socket = socket(PF_INET, SOCK_DGRAM, 0); + if (radio_sound_mic_socket != INVALID_SOCKET) { + setsockopt(radio_sound_mic_socket, SOL_SOCKET, SO_SNDBUF, (char *)&sndsize, sizeof(sndsize)); + Addr.sin_family = AF_INET; + Addr.sin_port = htons(port); +#ifdef MS_WINDOWS + Addr.sin_addr.S_un.S_addr = inet_addr(radio_sound_mic_ip); +#else + inet_aton(radio_sound_mic_ip, &Addr.sin_addr); +#endif + if (connect(radio_sound_mic_socket, (const struct sockaddr *)&Addr, sizeof(Addr)) != 0) { + close(radio_sound_mic_socket); + radio_sound_mic_socket = INVALID_SOCKET; + } + } + if (radio_sound_mic_socket == INVALID_SOCKET) { + printf("open_radio_sound_mic_socket: Failure to open socket\n"); + } + else { +#if DEBUG_IO + printf("open_radio_sound_mic_socket: opened socket %s\n", radio_sound_mic_ip); +#endif + } + } +} + +PyObject * quisk_set_sound_name(PyObject * self, PyObject * args) // Called from GUI thread +{ + char * description; // The description from quisk.py + char * device_name; // The device name from quisk.py + const char * utf8 = "utf-8"; + int index, play, driver; + Py_ssize_t l1, l2; + + description = malloc(QUISK_SC_SIZE); + device_name = malloc(QUISK_SC_SIZE); + l1 = l2 = QUISK_SC_SIZE; + if (!PyArg_ParseTuple (args, "iiies#es#", &index, &play, &driver, utf8, &description, &l1, utf8, &device_name, &l2)) + return NULL; + if (play) { + strMcpy(quiskPlaybackDevices[index]->name, description, QUISK_SC_SIZE); + strMcpy(quiskPlaybackDevices[index]->device_name, device_name, QUISK_SC_SIZE); + quiskPlaybackDevices[index]->driver = driver; + } + else { + strMcpy(CaptureDevices[index]->name, description, QUISK_SC_SIZE); + strMcpy(CaptureDevices[index]->device_name, device_name, QUISK_SC_SIZE); + CaptureDevices[index]->driver = driver; + } + free(description); + free(device_name); + Py_INCREF (Py_None); + return Py_None; +} + +static void set_digital_rx() // Set playback devices for digital modes from sub-receivers +{ + int i; + char buf80[80]; + struct sound_dev * rx_dev; + + for (i = QUISK_INDEX_SUB_RX1; i < QUISK_INDEX_SUB_RX1 + QUISK_MAX_SUB_RECEIVERS; i++) { + rx_dev = quiskPlaybackDevices[i]; + rx_dev->dev_errmsg[0] = 0; + rx_dev->dev_index = t_DigitalRx1Output; + snprintf(buf80, 80, "Digital Rx%d Output", i - QUISK_INDEX_SUB_RX1 + 1); + strMcpy(rx_dev->stream_description, buf80, QUISK_SC_SIZE); + rx_dev->sample_rate = 48000; + rx_dev->channel_I = 0; + rx_dev->channel_Q = 1; + set_num_channels (rx_dev); + rx_dev->average_square = 0; + rx_dev->stream_dir_record = 0; + rx_dev->read_frames = 0; + rx_dev->latency_frames = rx_dev->sample_rate * quisk_sound_state.latency_millisecs / 1000; + } +} + +void quisk_open_sound(void) // Called from GUI thread +{ + int i; + struct sound_dev * pPlay; + + quisk_play_state = SHUTDOWN; + quisk_sound_state.read_error = 0; + quisk_sound_state.write_error = 0; + quisk_sound_state.underrun_error = 0; + quisk_sound_state.mic_read_error = 0; + quisk_sound_state.interrupts = 0; + quisk_sound_state.rate_min = quisk_sound_state.rate_max = -99; + quisk_sound_state.chan_min = quisk_sound_state.chan_max = -99; + quisk_sound_state.msg1[0] = 0; + quisk_sound_state.err_msg[0] = 0; + Capture.dev_errmsg[0] = 0; + quisk_Playback.dev_errmsg[0] = 0; + MicCapture.dev_errmsg[0] = 0; + quisk_MicPlayback.dev_errmsg[0] = 0; + DigitalInput.dev_errmsg[0] = 0; + DigitalOutput.dev_errmsg[0] = 0; + RawSamplePlayback.dev_errmsg[0] = 0; + Capture.dev_index = t_Capture; + quisk_Playback.dev_index = t_Playback; + MicCapture.dev_index = t_MicCapture; + quisk_MicPlayback.dev_index = t_MicPlayback; + DigitalInput.dev_index = t_DigitalInput; + DigitalOutput.dev_index = t_DigitalOutput; + RawSamplePlayback.dev_index = t_RawSamplePlayback; + + set_digital_rx(); + + // Set stream descriptions. This is important for "deviceless" drivers like + // PulseAudio to be able to distinguish the streams from each other. + strMcpy(Capture.stream_description, "I/Q Rx Sample Input", QUISK_SC_SIZE); + Capture.stream_description[QUISK_SC_SIZE-1] = '\0'; + strMcpy(quisk_Playback.stream_description, "Radio Sound Output", QUISK_SC_SIZE); + quisk_Playback.stream_description[QUISK_SC_SIZE-1] = '\0'; + strMcpy(MicCapture.stream_description, "Microphone Input", QUISK_SC_SIZE); + MicCapture.stream_description[QUISK_SC_SIZE-1] = '\0'; + strMcpy(quisk_MicPlayback.stream_description, "I/Q Tx Sample Output", QUISK_SC_SIZE); + quisk_MicPlayback.stream_description[QUISK_SC_SIZE-1] = '\0'; + strMcpy(DigitalInput.stream_description, "Digital Tx0 Input", QUISK_SC_SIZE); + strMcpy(DigitalOutput.stream_description, "Digital Rx0 Output", QUISK_SC_SIZE); + strMcpy(RawSamplePlayback.stream_description, "Raw Digital Output", QUISK_SC_SIZE); + + quisk_Playback.sample_rate = quisk_sound_state.playback_rate; // Radio sound play rate + quisk_MicPlayback.sample_rate = quisk_sound_state.mic_playback_rate; + MicCapture.sample_rate = quisk_sound_state.mic_sample_rate; + MicCapture.channel_I = quisk_sound_state.mic_channel_I; // Mic audio is here + MicCapture.channel_Q = quisk_sound_state.mic_channel_Q; + // Capture device for digital modes + DigitalInput.sample_rate = 48000; + DigitalInput.channel_I = 0; + DigitalInput.channel_Q = 1; + // Playback device for digital modes + digital_output_level = QuiskGetConfigDouble("digital_output_level", 0.7); + DigitalOutput.sample_rate = quisk_sound_state.playback_rate; // Radio sound play rate + DigitalOutput.channel_I = 0; + DigitalOutput.channel_Q = 1; + // Playback device for raw samples + RawSamplePlayback.sample_rate = quisk_sound_state.sample_rate; + RawSamplePlayback.channel_I = 0; + RawSamplePlayback.channel_Q = 1; + + set_num_channels (&Capture); + set_num_channels (&quisk_Playback); + set_num_channels (&MicCapture); + set_num_channels (&quisk_MicPlayback); + set_num_channels (&DigitalInput); + set_num_channels (&DigitalOutput); + set_num_channels (&RawSamplePlayback); + + for (i = 0; (pPlay = quiskPlaybackDevices[i]); i++) { + pPlay->started = 0; + pPlay->cr_correction = 0; + pPlay->cr_delay = 3; + pPlay->cr_average_fill = 0; + pPlay->cr_average_count = 0; + pPlay->cr_sample_time = 0; + pPlay->cr_correct_time = 0; + } + + Capture.average_square = 0; + quisk_Playback.average_square = 0; + MicCapture.average_square = 0; + quisk_MicPlayback.average_square = 0; + DigitalInput.average_square = 0; + DigitalOutput.average_square = 0; + RawSamplePlayback.average_square = 0; + + //Needed for pulse audio context connection (KM4DSJ) + Capture.stream_dir_record = 1; + quisk_Playback.stream_dir_record = 0; + MicCapture.stream_dir_record = 1; + quisk_MicPlayback.stream_dir_record= 0; + DigitalInput.stream_dir_record = 1; + DigitalOutput.stream_dir_record = 0; + RawSamplePlayback.stream_dir_record = 0; + + //For remote IQ server over pulseaudio (KM4DSJ) + if (quisk_sound_state.IQ_server[0]) { + strMcpy(Capture.server, quisk_sound_state.IQ_server, IP_SIZE); + strMcpy(quisk_MicPlayback.server, quisk_sound_state.IQ_server, IP_SIZE); + } + + +#ifdef FIX_H101 + Capture.channel_Delay = Capture.channel_Q; // Obsolete; do not use. +#else + Capture.channel_Delay = QuiskGetConfigInt ("channel_delay", -1); +#endif + quisk_MicPlayback.channel_Delay = QuiskGetConfigInt ("tx_channel_delay", -1); + + if (pt_sample_read) // capture from SDR-IQ by Rf-Space or UDP + Capture.name[0] = 0; // zero the capture soundcard name + else // sound card capture + Capture.sample_rate = quisk_sound_state.sample_rate; + // set read size for sound card capture + i = (int)(quisk_sound_state.data_poll_usec * 1e-6 * Capture.sample_rate + 0.5); + if (i > SAMP_BUFFER_SIZE / Capture.num_channels) // limit to buffer size + i = SAMP_BUFFER_SIZE / Capture.num_channels; + Capture.read_frames = i; + MicCapture.read_frames = 0; // Use non-blocking read for microphone + quisk_Playback.read_frames = 0; + quisk_MicPlayback.read_frames = 0; + // set sound card play latency + quisk_Playback.latency_frames = quisk_Playback.sample_rate * quisk_sound_state.latency_millisecs / 1000; + quisk_MicPlayback.latency_frames = quisk_MicPlayback.sample_rate * quisk_sound_state.latency_millisecs / 1000; + Capture.latency_frames = 0; + MicCapture.latency_frames = 0; + // set capture and playback for digital modes + DigitalInput.read_frames = 0; // Use non-blocking read + DigitalInput.latency_frames = 0; + DigitalOutput.read_frames = 0; + DigitalOutput.latency_frames = DigitalOutput.sample_rate * quisk_sound_state.latency_millisecs / 1000; + // set capture and playback for raw samples + RawSamplePlayback.read_frames = 0; + RawSamplePlayback.latency_frames = RawSamplePlayback.sample_rate * quisk_sound_state.latency_millisecs / 1000; + open_radio_sound_socket(); +#if DEBUG_IO + printf("Sample buffer size %d, latency msec %d\n", SAMP_BUFFER_SIZE, quisk_sound_state.latency_millisecs); +#endif +} + +void quisk_start_sound(void) // Called from sound thread +{ + if (pt_sample_start) + (*pt_sample_start)(); + + // Let the drivers see the devices and start them up if appropriate + quisk_start_sound_directx(CaptureDevices, quiskPlaybackDevices); + quisk_start_sound_wasapi(CaptureDevices, quiskPlaybackDevices); + quisk_start_sound_portaudio(CaptureDevices, quiskPlaybackDevices); + quisk_start_sound_pulseaudio(CaptureDevices, quiskPlaybackDevices); + quisk_start_sound_alsa(CaptureDevices, quiskPlaybackDevices); + if (pt_sample_read) { // Capture from SDR-IQ or UDP + quisk_sound_state.rate_min = quisk_Playback.rate_min; + quisk_sound_state.rate_max = quisk_Playback.rate_max; + quisk_sound_state.chan_min = quisk_Playback.chan_min; + quisk_sound_state.chan_max = quisk_Playback.chan_max; + } + else { // Capture from sound card + quisk_sound_state.rate_min = Capture.rate_min; + quisk_sound_state.rate_max = Capture.rate_max; + quisk_sound_state.chan_min = Capture.chan_min; + quisk_sound_state.chan_max = Capture.chan_max; + } + QuiskDeltaSec(0); // Set timer to zero + QuiskDeltaSec(1); + quisk_set_play_state(); + quisk_play_state = STARTING; +} + +PyObject * quisk_set_ampl_phase(PyObject * self, PyObject * args) // Called from GUI thread +{ /* Set the sound card amplitude and phase corrections. See + S.W. Ellingson, Correcting I-Q Imbalance in Direct Conversion Receivers, February 10, 2003 */ + struct sound_dev * dev; + double ampl, phase; + int is_tx; // Is this for Tx? Otherwise Rx. + + if (!PyArg_ParseTuple (args, "ddi", &l, &phase, &is_tx)) + return NULL; + if (is_tx) + dev = &quisk_MicPlayback; + else + dev = &Capture; + if (ampl == 0.0 && phase == 0.0) { + dev->doAmplPhase = 0; + } + else { + dev->doAmplPhase = 1; + ampl = ampl + 1.0; // Change factor 0.01 to 1.01 + phase = (phase / 360.0) * 2.0 * M_PI; // convert to radians + dev->AmPhAAAA = 1.0 / ampl; + dev->AmPhCCCC = - dev->AmPhAAAA * tan(phase); + dev->AmPhDDDD = 1.0 / cos(phase); + } + Py_INCREF (Py_None); + return Py_None; +} + +PyObject * quisk_capt_channels(PyObject * self, PyObject * args) // Called from GUI thread +{ + if (!PyArg_ParseTuple (args, "ii", &Capture.channel_I, &Capture.channel_Q)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +PyObject * quisk_play_channels(PyObject * self, PyObject * args) // Called from GUI thread +{ + if (!PyArg_ParseTuple (args, "ii", &quisk_Playback.channel_I, &quisk_Playback.channel_Q)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +PyObject * quisk_micplay_channels(PyObject * self, PyObject * args) // Called from GUI thread +{ + if (!PyArg_ParseTuple (args, "ii", &quisk_MicPlayback.channel_I, &quisk_MicPlayback.channel_Q)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +PyObject * quisk_set_sparams(PyObject * self, PyObject * args, PyObject * keywds) +{ /* Call with keyword arguments ONLY; change local parameters */ + static char * kwlist[] = {"dc_remove_bw", "digital_output_level", "remote_control_slave", "remote_control_head", + NULL} ; + + if (!PyArg_ParseTupleAndKeywords (args, keywds, "|idii", kwlist, &dc_remove_bw, &digital_output_level, + &remote_control_slave, &remote_control_head)) + return NULL; + Py_INCREF (Py_None); + return Py_None; +} + +PyObject * quisk_dummy_sound_devices(PyObject * self, PyObject * args) +{ // Return an empty list of sound devices + PyObject * pylist, * pycapt, * pyplay; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + pylist = PyList_New(0); // list [pycapt, pyplay] + pycapt = PyList_New(0); // list of capture devices + pyplay = PyList_New(0); // list of play devices + PyList_Append(pylist, pycapt); + PyList_Append(pylist, pyplay); + return pylist; +} + +void quisk_udp_mic_error(char * msg) +{ + MicCapture.dev_error++; +#if DEBUG_IO + printf("%s\n", msg); +#endif +} + +static void AddCard(struct sound_dev * dev, PyObject * pylist) +{ + PyObject * v; + + if (dev->name[0]) { + v = Py_BuildValue("(NNiiidN)", + PyUnicode_DecodeUTF8(dev->stream_description, strlen(dev->stream_description), "replace"), + PyUnicode_DecodeUTF8(dev->name, strlen(dev->name), "replace"), + dev->sample_rate, dev->dev_latency, dev->dev_error + dev->dev_underrun, dev->average_square, + PyUnicode_DecodeUTF8(dev->dev_errmsg, strlen(dev->dev_errmsg), "replace")); + PyList_Append(pylist, v); + } +} + +PyObject * quisk_sound_errors(PyObject * self, PyObject * args) +{ // return a list of strings with card names and error counts + PyObject * pylist; + struct sound_dev * rx_dev; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + pylist = PyList_New(0); + AddCard(&Capture, pylist); + AddCard(&MicCapture, pylist); + AddCard(&quisk_Playback, pylist); + AddCard(&quisk_MicPlayback, pylist); + AddCard(&RawSamplePlayback, pylist); + AddCard(&DigitalInput, pylist); + AddCard(&DigitalOutput, pylist); + rx_dev = quiskPlaybackDevices[QUISK_INDEX_SUB_RX1]; + AddCard(rx_dev, pylist); + return pylist; +} + +PyObject * quisk_set_file_name(PyObject * self, PyObject * args, PyObject * keywds) // called from GUI +{ + int which = -1; + const char * name = NULL; + int enable = -1; + int play_button = -1; + int record_button = -1; + static char * kwlist[] = {"which", "name", "enable", "play_button", "record_button", NULL} ; + + if (!PyArg_ParseTupleAndKeywords (args, keywds, "|isiii", kwlist, &which, &name, &enable, &play_button, &record_button)) + return NULL; + if (record_button == 0) { // Close all recording files + close_file_rec = 1; + } + if (play_button == 0) { // Close all play files + quisk_close_file_play = 1; + } + if (record_button == 1) switch (which) { + case 0: // record audio file + if (name) + strMcpy(file_rec_audio.file_name, name, QUISK_PATH_SIZE); + quisk_record_audio(&file_rec_audio, NULL, -1); + break; + case 1: // record sample file + if (name) + strMcpy(file_rec_samples.file_name, name, QUISK_PATH_SIZE); + record_samples(&file_rec_samples, NULL, -1); + break; + case 2: // record mic file + if (name) + strMcpy(file_rec_mic.file_name, name, QUISK_PATH_SIZE); + quisk_record_audio(&file_rec_mic, NULL, -1); + break; + case 10: // play audio file + break; + case 11: // play samples file + break; + case 12: // play CQ message file + break; + } + Py_INCREF (Py_None); + return Py_None; +} diff --git a/sound_alsa.c b/sound_alsa.c new file mode 100644 index 0000000..c4fbcbe --- /dev/null +++ b/sound_alsa.c @@ -0,0 +1,1272 @@ +/* + * This modue provides sound access for QUISK using the ALSA + * library for Linux. +*/ +#ifdef QUISK_HAVE_ALSA +#include +#include +#include +#include +#include "quisk.h" + +/* + The sample rate is in frames per second. Each frame has a number of channels, + and each channel has a sample of size sample_bytes. The channels are interleaved: + (channel0, channel1), (channel0, channel1), ... +*/ + +extern struct sound_conf quisk_sound_state; // Current sound status + +static int is_little_endian; // Test byte order; is it little-endian? +static double mic_playbuf_util = 0.70; // Current mic play buffer utilization 0.0 to 1.0 +static union { + short buffer2[SAMP_BUFFER_SIZE]; // Buffer for 2-byte samples from sound + unsigned char buffer3[3 * SAMP_BUFFER_SIZE]; // Buffer for 3-byte samples from sound + int buffer4[SAMP_BUFFER_SIZE]; // Buffer for 4-byte samples from sound +} bufs ; +static int bufferz[SAMP_BUFFER_SIZE]; // Buffer for zero samples + +static snd_pcm_sframes_t frames_in_buffer(struct sound_dev * dev) +{ // return the number of frames in the play buffer + snd_pcm_sframes_t avail_to_write; + + if ((avail_to_write = snd_pcm_avail(dev->handle)) < 0) { + dev->dev_error++; + if (quisk_sound_state.verbose_sound) + printf("frames_in_buffer: Failure for pcm_avail\n"); + return -1; + } + return dev->play_buf_size - avail_to_write; +} + +static snd_pcm_sframes_t write_frames(struct sound_dev * dev, void * buffer, int count) +{ + snd_pcm_sframes_t frames; + + if (count <= 0) + return 0; + frames = snd_pcm_writei (dev->handle, buffer, count); + if (frames <= 0) { + if (frames == -EPIPE) { // underrun + quisk_sound_state.underrun_error++; + dev->dev_underrun++; + if (quisk_sound_state.verbose_sound) + printf("Underrun %s\n", dev->stream_description); + } + else { + quisk_sound_state.write_error++; + dev->dev_error++; + if (quisk_sound_state.verbose_sound) + printf("Error write_frames %s\n", dev->stream_description); + } + snd_pcm_prepare(dev->handle); + frames = snd_pcm_writei (dev->handle, buffer, count); + } + return frames; +} + +int quisk_read_alsa(struct sound_dev * dev, complex double * cSamples) +{ // cSamples can be NULL to discard samples. + // Read sound samples from the ALSA soundcard. + // Samples are converted to 32 bits with a range of +/- CLIP32 and placed into cSamples. + int i; + snd_pcm_sframes_t frames, delay, avail; + short si, sq; + int ii, qq; + int nSamples; + + if (!dev->handle) + return -1; + + switch(snd_pcm_state(dev->handle)) { + case SND_PCM_STATE_RUNNING: + break; + case SND_PCM_STATE_PREPARED: + break; + case SND_PCM_STATE_XRUN: +#if DEBUG_IO + QuiskPrintTime("read_alsa: Capture overrun", 0); +#endif + snd_pcm_prepare(dev->handle); + break; + default: +#if DEBUG_IO + QuiskPrintTime("read_alsa: State UNKNOWN", 0); +#endif + break; + } + + if (snd_pcm_avail_delay(dev->handle, &avail, &delay) >= 0) { + dev->dev_latency = avail + delay; // avail frames can be read plus delay frames digitized but can't be read yet + } + else { + avail = 32; + dev->dev_latency = 0; + dev->dev_error++; +#if DEBUG_IO + QuiskPrintTime("read_alsa: snd_pcm_avail_delay failed", 0); +#endif + } + if (dev->read_frames == 0) { // non-blocking: read available frames + if (avail < 32) + avail = 32; // read frames to restart from error + } + else { + avail = dev->read_frames; // size of read request + } + i = SAMP_BUFFER_SIZE * 8 / 10 / dev->num_channels; // limit read request to buffer size + if (avail > i) + avail = i; + nSamples = 0; + switch (dev->sample_bytes) { + case 2: + frames = snd_pcm_readi (dev->handle, bufs.buffer2, avail); // read samples + if ( ! cSamples) + return 0; + if (frames == -EAGAIN) { // no samples available + break; + } + else if (frames <= 0) { // error + dev->dev_error++; +#if DEBUG_IO + QuiskPrintTime("read_alsa: frames < 0", 0); +#endif + snd_pcm_prepare (dev->handle); + snd_pcm_start (dev->handle); + break; + } + for (i = 0; frames; i += dev->num_channels, frames--) { + si = bufs.buffer2[i + dev->channel_I]; + sq = bufs.buffer2[i + dev->channel_Q]; + if (si >= CLIP16 || si <= -CLIP16) + dev->overrange++; // assume overrange returns max int + if (sq >= CLIP16 || sq <= -CLIP16) + dev->overrange++; + ii = si << 16; + qq = sq << 16; + cSamples[nSamples] = ii + I * qq; + nSamples++; + } + break; + case 3: + frames = snd_pcm_readi (dev->handle, bufs.buffer3, avail); // read samples + if ( ! cSamples) + return 0; + if (frames == -EAGAIN) { // no samples available + break; + } + else if (frames <= 0) { // error + dev->dev_error++; +#if DEBUG_IO + QuiskPrintTime("read_alsa: frames < 0", 0); +#endif + snd_pcm_prepare (dev->handle); + snd_pcm_start (dev->handle); + break; + } + for (i = 0; frames; i += dev->num_channels, frames--) { + ii = qq = 0; + if (!is_little_endian) { // convert to big-endian + *((unsigned char *)&ii ) = bufs.buffer3[(i + dev->channel_I) * 3 + 2]; + *((unsigned char *)&ii + 1) = bufs.buffer3[(i + dev->channel_I) * 3 + 1]; + *((unsigned char *)&ii + 2) = bufs.buffer3[(i + dev->channel_I) * 3 ]; + *((unsigned char *)&qq ) = bufs.buffer3[(i + dev->channel_Q) * 3 + 2]; + *((unsigned char *)&qq + 1) = bufs.buffer3[(i + dev->channel_Q) * 3 + 1]; + *((unsigned char *)&qq + 2) = bufs.buffer3[(i + dev->channel_Q) * 3 ]; + } + else { // convert to little-endian + memcpy((unsigned char *)&ii + 1, bufs.buffer3 + (i + dev->channel_I) * 3, 3); + memcpy((unsigned char *)&qq + 1, bufs.buffer3 + (i + dev->channel_Q) * 3, 3); + } + if (ii >= CLIP32 || ii <= -CLIP32) + dev->overrange++; // assume overrange returns max int + if (qq >= CLIP32 || qq <= -CLIP32) + dev->overrange++; + cSamples[nSamples] = ii + I * qq; + nSamples++; + } + break; + case 4: + frames = snd_pcm_readi (dev->handle, bufs.buffer4, avail); // read samples + if ( ! cSamples) + return 0; + if (frames == -EAGAIN) { // no samples available + break; + } + else if (frames <= 0) { // error + dev->dev_error++; +#if DEBUG_IO + QuiskPrintTime("read_alsa: frames < 0", 0); +#endif + snd_pcm_prepare (dev->handle); + snd_pcm_start (dev->handle); + break; + } + for (i = 0; frames; i += dev->num_channels, frames--) { + ii = bufs.buffer4[i + dev->channel_I]; + qq = bufs.buffer4[i + dev->channel_Q]; + if (ii >= CLIP32 || ii <= -CLIP32) + dev->overrange++; // assume overrange returns max int + if (qq >= CLIP32 || qq <= -CLIP32) + dev->overrange++; + cSamples[nSamples] = ii + I * qq; + nSamples++; + } + break; + default: + return 0; + } + if ( ! strcmp(dev->stream_description, "Microphone Input")) { + if (mic_playbuf_util > 0.85) { // Remove a sample + nSamples--; +#if DEBUG_IO + printf("read_alsa %s: Remove a mic sample, util %.2lf\n", dev->stream_description, mic_playbuf_util); +#endif + } + else if(cSamples && mic_playbuf_util < 0.55 && nSamples >= 2) { // Add a sample + cSamples[nSamples] = cSamples[nSamples - 1]; + cSamples[nSamples - 1] = (cSamples[nSamples - 2] + cSamples[nSamples]) / 2.0; + nSamples++; +#if DEBUG_IO + printf("read_alsa %s: Add a mic sample, util %.2lf\n", dev->stream_description, mic_playbuf_util); +#endif + } + } + return nSamples; +} + +void quisk_alsa_sidetone(struct sound_dev * dev) +{ + int i, bytes_per_sample, bytes_per_frame, ch_I, ch_Q, new_key; + snd_pcm_sframes_t frames, nFrames, rewindable; + snd_pcm_uframes_t buffer_size, period_size; + void * ptSample; + unsigned char * buffer; + + if ( ! dev->handle) + return; + if (snd_pcm_state(dev->handle) == SND_PCM_STATE_XRUN) { + if (quisk_sound_state.verbose_sound) + printf("alsa_sidetone: underrun\n"); + quisk_sound_state.underrun_error++; + dev->dev_underrun++; + snd_pcm_prepare(dev->handle); + } + if (snd_pcm_get_params (dev->handle, &buffer_size, &period_size) != 0) { + dev->dev_error++; + if (quisk_sound_state.verbose_sound) + printf("alsa_sidetone: Failure for get_params\n"); + return; + } + nFrames = dev->latency_frames - frames_in_buffer(dev); // write desired latency less fill level frames + new_key = QUISK_CWKEY_DOWN; + if (new_key != dev->old_key) { // key changed, empty buffer and refill + dev->old_key = new_key; + rewindable = snd_pcm_rewindable(dev->handle); + rewindable -= period_size; + if (rewindable > 0) { + snd_pcm_rewind(dev->handle, rewindable); + nFrames = dev->latency_frames - period_size; + quisk_make_sidetone(dev, rewindable); + } + } + if (nFrames <= 0) + return; + bytes_per_sample = dev->sample_bytes; + bytes_per_frame = dev->sample_bytes * dev->num_channels; + buffer = (unsigned char *)bufs.buffer4; + ch_I = dev->channel_I; + ch_Q = dev->channel_Q; + for (i = 0; i < nFrames; i++) { + ptSample = quisk_make_sidetone(dev, 0); + memcpy(buffer + ch_I * bytes_per_sample, ptSample, bytes_per_sample); + memcpy(buffer + ch_Q * bytes_per_sample, ptSample, bytes_per_sample); + buffer += bytes_per_frame; + } + frames = write_frames(dev, bufs.buffer4, nFrames); + if (quisk_sound_state.verbose_sound && (frames != nFrames)) + printf("alsa_sidetone: %s bad write %ld %ld\n", dev->stream_description, nFrames, frames); +} + +void quisk_play_alsa(struct sound_dev * playdev, int nSamples, + complex double * cSamples, int report_latency, double volume) +{ // Play the samples; write them to the ALSA soundcard. + int i, n, index, buffer_frames; + snd_pcm_sframes_t frames, rewind; + int ii, qq; + +#if DEBUG_IO + static int timer=0; +#endif + + if (!playdev->handle || nSamples <= 0) + return; + if (snd_pcm_state(playdev->handle) == SND_PCM_STATE_XRUN) { + if (quisk_sound_state.verbose_sound) + printf("play_alsa: underrun on %s\n", playdev->stream_description); + quisk_sound_state.underrun_error++; + playdev->dev_underrun++; + snd_pcm_prepare(playdev->handle); + } + buffer_frames = frames_in_buffer(playdev); + playdev->dev_latency = buffer_frames; + if (report_latency) { // Report for main playback device + quisk_sound_state.latencyPlay = buffer_frames; // samples in play buffer + } + playdev->cr_average_fill += (double)(buffer_frames + nSamples / 2) / playdev->play_buf_size; + playdev->cr_average_count++; + if (playdev->dev_index == t_MicPlayback) + mic_playbuf_util = (double)(nSamples + buffer_frames) / playdev->play_buf_size; +#if DEBUG_IO + timer += nSamples; + if (timer > playdev->sample_rate) { + timer = 0; + printf("play_alsa %s: Samples new %d old %d total %d latency_frames %d\n", + playdev->stream_description, nSamples, buffer_frames, nSamples + buffer_frames, playdev->latency_frames); + } +#endif + + + if (nSamples + buffer_frames > playdev->play_buf_size) { // rewind some frames to go back to the fill level latency_frames + rewind = nSamples + buffer_frames - playdev->latency_frames; + if (rewind > buffer_frames) + rewind = buffer_frames; + snd_pcm_rewind(playdev->handle, rewind); + quisk_sound_state.write_error++; + playdev->dev_error++; + if (quisk_sound_state.verbose_sound) + printf("play_alsa: Buffer overflow in %s\n", playdev->stream_description); + } + index = 0; + switch(playdev->sound_format) { + case Int16: + while (index < nSamples) { + for (i = 0, n = index; n < nSamples; i += playdev->num_channels, n++) { + ii = (int)(volume * creal(cSamples[n]) / 65536); + qq = (int)(volume * cimag(cSamples[n]) / 65536); + bufs.buffer2[i + playdev->channel_I] = (short)ii; + bufs.buffer2[i + playdev->channel_Q] = (short)qq; + } + n = n - index; + frames = write_frames(playdev, bufs.buffer2, n); + if (frames <= 0) + index = nSamples; // give up + else + index += frames; + } + break; + case Int24: + while (index < nSamples) { + for (i = 0, n = index; n < nSamples; i += playdev->num_channels, n++) { + ii = (int)(volume * creal(cSamples[n]) / 256); + qq = (int)(volume * cimag(cSamples[n]) / 256); + if (!is_little_endian) { // convert to big-endian + bufs.buffer3[(i + playdev->channel_I) * 3 ] = *((unsigned char *)&ii + 2); + bufs.buffer3[(i + playdev->channel_Q) * 3 ] = *((unsigned char *)&qq + 2); + bufs.buffer3[(i + playdev->channel_I) * 3 + 1] = *((unsigned char *)&ii + 1); + bufs.buffer3[(i + playdev->channel_Q) * 3 + 1] = *((unsigned char *)&qq + 1); + bufs.buffer3[(i + playdev->channel_I) * 3 + 2] = *((unsigned char *)&ii ); + bufs.buffer3[(i + playdev->channel_Q) * 3 + 2] = *((unsigned char *)&qq ); + } + else { // convert to little-endian + memcpy(bufs.buffer3 + (i + playdev->channel_I) * 3, (unsigned char *)&ii, 3); + memcpy(bufs.buffer3 + (i + playdev->channel_Q) * 3, (unsigned char *)&qq, 3); + } + } + n = n - index; + frames = write_frames(playdev, bufs.buffer3, n); + if (frames <= 0) + index = nSamples; // give up + else + index += frames; + } + break; + case Int32: + while (index < nSamples) { + for (i = 0, n = index; n < nSamples; i += playdev->num_channels, n++) { + ii = (int)(volume * creal(cSamples[n])); + qq = (int)(volume * cimag(cSamples[n])); + bufs.buffer4[i + playdev->channel_I] = ii; + bufs.buffer4[i + playdev->channel_Q] = qq; + } + n = n - index; + frames = write_frames(playdev, bufs.buffer4, n); + if (frames <= 0) + index = nSamples; // give up + else + index += frames; + } + break; + case Float32: + break; + } +} + +static int device_list(PyObject * py, snd_pcm_stream_t stream, char * name) +{ // return 1 if the card name was substituted + snd_ctl_t *handle; + int card, err, dev; + char buf100[100]; + const char * card_text, * pcm_text; + snd_ctl_card_info_t *info; + snd_pcm_info_t *pcminfo; + + snd_ctl_card_info_alloca(&info); + snd_pcm_info_alloca(&pcminfo); + + card = -1; + if (snd_card_next(&card) < 0 || card < 0) { + printf("no soundcards found...\n"); + return 0; + } + while (card >= 0) { + sprintf(buf100, "hw:%d", card); + if ((err = snd_ctl_open(&handle, buf100, 0)) < 0) { + printf("device_list: control open (%i): %s", card, snd_strerror(err)); + goto next_card; + } + if ((err = snd_ctl_card_info(handle, info)) < 0) { + printf("device_list: control hardware info (%i): %s", card, snd_strerror(err)); + snd_ctl_close(handle); + goto next_card; + } + dev = -1; + while (1) { + if (snd_ctl_pcm_next_device(handle, &dev)<0) + printf("device_list: snd_ctl_pcm_next_device\n"); + if (dev < 0) + break; + snd_pcm_info_set_device(pcminfo, dev); + snd_pcm_info_set_subdevice(pcminfo, 0); + snd_pcm_info_set_stream(pcminfo, stream); + card_text = snd_ctl_card_info_get_name(info); + if ( ! card_text || ! card_text[0]) + card_text = snd_ctl_card_info_get_id(info); + if ((err = snd_ctl_pcm_info(handle, pcminfo)) < 0) { + if (err != -ENOENT) + printf ("device_list: control digital audio info (%i): %s", card, snd_strerror(err)); + continue; + } + else { + pcm_text = snd_pcm_info_get_name(pcminfo); + if ( ! pcm_text || ! pcm_text[0]) + pcm_text = snd_pcm_info_get_id(pcminfo); + } + snprintf(buf100, 100, "%s %s (hw:%d,%d)", card_text, pcm_text, card, dev); + if (py) { // add to list of devices + PyList_Append(py, PyString_FromString(buf100)); + } + if (name) { // return the "hw:" name + if (strstr(buf100, name)) { + snprintf(name, QUISK_SC_SIZE, "hw:%d,%d", card, dev); + snd_ctl_close(handle); + return 1; + } + } + } + snd_ctl_close(handle); + next_card: + if (snd_card_next(&card) < 0) { + printf("snd_card_next\n"); + break; + } + } + return 0; +} + +PyObject * quisk_alsa_sound_devices(PyObject * self, PyObject * args) +{ // Return a list of ALSA device names [pycapt, pyplay] + PyObject * pylist, * pycapt, * pyplay; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + // Each pycapt and pyplay is [pydev, pyname] + pylist = PyList_New(0); // list [pycapt, pyplay] + pycapt = PyList_New(0); // list of capture devices + pyplay = PyList_New(0); // list of play devices + PyList_Append(pylist, pycapt); + PyList_Append(pylist, pyplay); + device_list(pycapt, SND_PCM_STREAM_CAPTURE, NULL); + device_list(pyplay, SND_PCM_STREAM_PLAYBACK, NULL); + return pylist; +} + +static snd_pcm_format_t check_formats(struct sound_dev * dev, snd_pcm_hw_params_t *hware) +{ + snd_pcm_format_t format = SND_PCM_FORMAT_UNKNOWN; + dev->sample_bytes = 0; + + strMcpy (dev->msg1, "Available formats: ", QUISK_SC_SIZE); + if (snd_pcm_hw_params_test_format (dev->handle, hware, SND_PCM_FORMAT_S32) == 0) { + if (!dev->sample_bytes) { + strncat(dev->msg1, "*", QUISK_SC_SIZE); + dev->sample_bytes = 4; + dev->sound_format = Int32; + format = SND_PCM_FORMAT_S32; + } + strncat(dev->msg1, "S32 ", QUISK_SC_SIZE); + } + if (snd_pcm_hw_params_test_format (dev->handle, hware, SND_PCM_FORMAT_U32) == 0) { + strncat(dev->msg1, "U32 ", QUISK_SC_SIZE); + } + if (snd_pcm_hw_params_test_format (dev->handle, hware, SND_PCM_FORMAT_S24) == 0) { + strncat(dev->msg1, "S24 ", QUISK_SC_SIZE); + } + if (snd_pcm_hw_params_test_format (dev->handle, hware, SND_PCM_FORMAT_U24) == 0) { + strncat(dev->msg1, "U24 ", QUISK_SC_SIZE); + } + if (snd_pcm_hw_params_test_format (dev->handle, hware, SND_PCM_FORMAT_S16) == 0) { + if (!dev->sample_bytes) { + strncat(dev->msg1, "*", QUISK_SC_SIZE); + dev->sample_bytes = 2; + dev->sound_format = Int16; + format = SND_PCM_FORMAT_S16; + } + strncat(dev->msg1, "S16 ", QUISK_SC_SIZE); + } + if (snd_pcm_hw_params_test_format (dev->handle, hware, SND_PCM_FORMAT_S24_3LE) == 0) { + if (!dev->sample_bytes) { + strncat(dev->msg1, "*", QUISK_SC_SIZE); + dev->sample_bytes = 3; + dev->sound_format = Int24; + format = SND_PCM_FORMAT_S24_3LE; + } + strncat(dev->msg1, "S24_3LE ", QUISK_SC_SIZE); + } + if (snd_pcm_hw_params_test_format (dev->handle, hware, SND_PCM_FORMAT_U16) == 0) { + strncat(dev->msg1, "U16 ", QUISK_SC_SIZE); + } + if (format == SND_PCM_FORMAT_UNKNOWN) + strncat(dev->msg1, "*UNSUPPORTED", QUISK_SC_SIZE); + else + snd_pcm_hw_params_set_format (dev->handle, hware, format); + return format; +} + +static int quisk_open_alsa_capture(struct sound_dev * dev) +{ // Open the ALSA soundcard for capture. Return non-zero for error. + int i, err, dir, sample_rate, mode; + int poll_size; + unsigned int ui; + char buf[QUISK_SC_SIZE]; + snd_pcm_hw_params_t *hware; + snd_pcm_sw_params_t *sware; + snd_pcm_uframes_t frames; + snd_pcm_t * handle; + + if ( ! dev->name[0]) // Check for null capture name + return 0; + + if (quisk_sound_state.verbose_sound) + printf("*** Capture %s on alsa name %s device %s\n", dev->stream_description, dev->name, dev->device_name); + if (dev->read_frames == 0) + mode = SND_PCM_NONBLOCK; + else + mode = 0; + if ( ! strncmp (dev->name, "alsa:", 5)) { // search for the name in info strings, put device name into buf + strMcpy(buf, dev->name + 5, QUISK_SC_SIZE); + device_list(NULL, SND_PCM_STREAM_CAPTURE, buf); + } + else { // just try to open the device + strMcpy(buf, dev->device_name, QUISK_SC_SIZE); + } + for (i = 0; i < 6; i++) { // try a few times in case the device is busy + if (quisk_sound_state.verbose_sound) + printf(" Try %d to open %s\n", i, buf); + err = snd_pcm_open (&handle, buf, SND_PCM_STREAM_CAPTURE, mode); + if (err >= 0) + break; + QuiskSleepMicrosec(500000); + } + if (err < 0) { + snprintf(quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot open capture device %.40s (%.40s)", + dev->name, snd_strerror (err)); + strMcpy(dev->dev_errmsg, quisk_sound_state.err_msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + printf("%s\n", quisk_sound_state.err_msg); + return 1; + } + dev->handle = handle; + dev->driver = DEV_DRIVER_ALSA; + dev->old_key = 0; + if ((err = snd_pcm_sw_params_malloc (&sware)) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot allocate software parameter structure (%s)\n", + snd_strerror (err)); + if (quisk_sound_state.verbose_sound) + printf("%s\n", quisk_sound_state.err_msg); + return 1; + } + if ((err = snd_pcm_hw_params_malloc (&hware)) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot allocate hardware parameter structure (%s)\n", + snd_strerror (err)); + if (quisk_sound_state.verbose_sound) + printf("%s\n", quisk_sound_state.err_msg); + snd_pcm_sw_params_free (sware); + return 1; + } + if ((err = snd_pcm_hw_params_any (handle, hware)) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot initialize capture parameters (%s)\n", + snd_strerror (err)); + goto errend; + } + /* UNAVAILABLE + if ((err = snd_pcm_hw_params_set_rate_resample (handle, hware, 0)) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot disable resampling (%s)\n", + snd_strerror (err)); + goto errend; + } + */ + // Get some parameters to send back + if (snd_pcm_hw_params_get_rate_min(hware, &dev->rate_min, &dir) != 0) + dev->rate_min = 0; // Error + if (snd_pcm_hw_params_get_rate_max(hware, &dev->rate_max, &dir) != 0) + dev->rate_max = 0; // Error + if (snd_pcm_hw_params_get_channels_min(hware, &dev->chan_min) != 0) + dev->chan_min= 0; // Error + if (snd_pcm_hw_params_get_channels_max(hware, &dev->chan_max) != 0) + dev->chan_max= 0; // Error + if (quisk_sound_state.verbose_sound) { + printf(" Sample rate min %d max %d\n", dev->rate_min, dev->rate_max); + printf(" Sample rate requested %d\n", dev->sample_rate); + printf(" Number of channels min %d max %d\n", dev->chan_min, dev->chan_max); + printf(" Capture channels are %d %d\n", dev->channel_I, dev->channel_Q); + } + // Set the capture parameters + if (check_formats(dev, hware) == SND_PCM_FORMAT_UNKNOWN) { + strMcpy(quisk_sound_state.msg1, dev->msg1, QUISK_SC_SIZE); + strMcpy (quisk_sound_state.err_msg, "Quisk does not support your capture format.", QUISK_SC_SIZE); + goto errend; + } + strMcpy(quisk_sound_state.msg1, dev->msg1, QUISK_SC_SIZE); + sample_rate = dev->sample_rate; + if (snd_pcm_hw_params_set_rate (handle, hware, sample_rate, 0) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Can not set sample rate %d", + sample_rate); + goto errend; + } + if (snd_pcm_hw_params_set_access (handle, hware, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) { + strMcpy(quisk_sound_state.err_msg, "Interleaved access is not available", QUISK_SC_SIZE); + goto errend; + } + if (snd_pcm_hw_params_get_channels_min(hware, &ui) != 0) + ui = 0; // Error + if (dev->num_channels < (int)ui) // increase number of channels to minimum available + dev->num_channels = ui; + if (snd_pcm_hw_params_set_channels (handle, hware, dev->num_channels) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Can not set channels to %d", dev->num_channels); + goto errend; + } + // Try to set a capture buffer larger than needed + frames = sample_rate * 200 / 1000; // buffer size in milliseconds + if (snd_pcm_hw_params_set_buffer_size_near (handle, hware, &frames) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Can not set capture buffer size"); + goto errend; + } + dev->play_buf_size = frames; // play_buf_size used for capture buffer size too + poll_size = (int)(quisk_sound_state.data_poll_usec * 1e-6 * sample_rate + 0.5); + if ((int)frames < poll_size * 3) { // buffer size is too small, reduce poll time + quisk_sound_state.data_poll_usec = (int)(frames * 1.e6 / sample_rate / 3 + 0.5); +#if DEBUG_IO + printf("Reduced data_poll_usec %d for small sound capture buffer\n", + quisk_sound_state.data_poll_usec); +#endif + } + if (quisk_sound_state.verbose_sound) { + printf(" %s\n", dev->msg1); + printf(" Capture buffer size %d\n", dev->play_buf_size); + if ((int)frames > SAMP_BUFFER_SIZE / dev->num_channels) + printf("Capture buffer exceeds size of sample buffers\n"); + } + if ((err = snd_pcm_hw_params (handle, hware)) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot set hw capture parameters (%s)\n", + snd_strerror (err)); + goto errend; + } + if ((err = snd_pcm_sw_params_current (handle, sware)) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot get software capture parameters (%s)\n", + snd_strerror (err)); + goto errend; + } + + if ((err = snd_pcm_prepare (handle)) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot prepare capture interface for use (%s)\n", + snd_strerror (err)); + goto errend; + } + // Success + snd_pcm_hw_params_free (hware); + snd_pcm_sw_params_free (sware); + if (quisk_sound_state.verbose_sound) + printf("*** End capture on alsa device %s %s\n", dev->name, quisk_sound_state.err_msg); + return 0; +errend: + snd_pcm_hw_params_free (hware); + snd_pcm_sw_params_free (sware); + if (quisk_sound_state.verbose_sound) { + printf("*** Error end for capture on alsa device %s %s\n", dev->name, quisk_sound_state.err_msg); + } + return 1; +} + +static int quisk_open_alsa_playback(struct sound_dev * dev) +{ // Open the ALSA soundcard for playback. Return non-zero on error. + int i, err, dir, mode; + unsigned int ui; + char buf[QUISK_SC_SIZE]; + snd_pcm_hw_params_t *hware; + snd_pcm_sw_params_t *sware; + snd_pcm_uframes_t frames, buffer_size, period_size; + snd_pcm_t * handle; + + if ( ! dev->name[0]) // Check for null play name + return 0; + + if (quisk_sound_state.verbose_sound) + printf("*** Playback %s, alsa name %s, device %s\n", dev->stream_description, dev->name, dev->device_name); + if (dev->read_frames == 0) + mode = SND_PCM_NONBLOCK; + else + mode = 0; + if ( ! strncmp (dev->name, "alsa:", 5)) { // search for the name in info strings, put device name into buf + strMcpy(buf, dev->name + 5, QUISK_SC_SIZE); + device_list(NULL, SND_PCM_STREAM_PLAYBACK, buf); + } + else { // just try to open the device + strMcpy(buf, dev->device_name, QUISK_SC_SIZE); + } + for (i = 0; i < 6; i++) { // try a few times in case the device is busy + if (quisk_sound_state.verbose_sound) + printf(" Try %d to open %s\n", i, buf); + err = snd_pcm_open (&handle, buf, SND_PCM_STREAM_PLAYBACK, mode); + if (err >= 0) + break; + QuiskSleepMicrosec(500000); + } + if (err < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot open playback device %.40s (%.40s)\n", + dev->name, snd_strerror (err)); + strMcpy(dev->dev_errmsg, quisk_sound_state.err_msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + printf("%s\n", quisk_sound_state.err_msg); + return 1; + } + dev->handle = handle; + dev->old_key = 0; + if ((err = snd_pcm_sw_params_malloc (&sware)) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot allocate software parameter structure (%s)\n", + snd_strerror (err)); + if (quisk_sound_state.verbose_sound) + printf("%s\n", quisk_sound_state.err_msg); + return 1; + } + if ((err = snd_pcm_hw_params_malloc (&hware)) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot allocate hardware parameter structure (%s)\n", + snd_strerror (err)); + if (quisk_sound_state.verbose_sound) + printf("%s\n", quisk_sound_state.err_msg); + snd_pcm_sw_params_free (sware); + return 1; + } + if ((err = snd_pcm_hw_params_any (handle, hware)) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot initialize playback parameter structure (%s)\n", + snd_strerror (err)); + goto errend; + } + // Get some parameters to send back + if (snd_pcm_hw_params_get_rate_min(hware, &dev->rate_min, &dir) != 0) + dev->rate_min = 0; // Error + if (snd_pcm_hw_params_get_rate_max(hware, &dev->rate_max, &dir) != 0) + dev->rate_max = 0; // Error + if (snd_pcm_hw_params_get_channels_min(hware, &dev->chan_min) != 0) + dev->chan_min= 0; // Error + if (snd_pcm_hw_params_get_channels_max(hware, &dev->chan_max) != 0) + dev->chan_max= 0; // Error + if (quisk_sound_state.verbose_sound) { + printf(" Sample rate min %d max %d\n", dev->rate_min, dev->rate_max); + printf(" Sample rate requested %d\n", dev->sample_rate); + printf(" Number of channels min %d max %d\n", dev->chan_min, dev->chan_max); + printf(" Play channels are %d %d\n", dev->channel_I, dev->channel_Q); + } + // Set the playback parameters + if (snd_pcm_hw_params_set_rate (handle, hware, dev->sample_rate, 0) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot set playback rate %d", + dev->sample_rate); + goto errend; + } + if (snd_pcm_hw_params_set_access (handle, hware, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot set playback access to interleaved."); + goto errend; + } + if (snd_pcm_hw_params_get_channels_min(hware, &ui) != 0) + ui = 0; // Error + if (dev->num_channels < (int)ui) // increase number of channels to minimum available + dev->num_channels = ui; + if (snd_pcm_hw_params_set_channels (handle, hware, dev->num_channels) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot set playback channels to %d", + dev->num_channels); + goto errend; + } + if (check_formats(dev, hware) == SND_PCM_FORMAT_UNKNOWN) { + strMcpy(quisk_sound_state.msg1, dev->msg1, QUISK_SC_SIZE); + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot set playback format."); + goto errend; + } + if (quisk_sound_state.verbose_sound) + printf(" %s\n", dev->msg1); + // Set the buffer size + frames = dev->latency_frames * 2; + if (snd_pcm_hw_params_set_buffer_size_near (handle, hware, &frames) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Can not set playback buffer size"); + goto errend; + } + dev->play_buf_size = frames; + dev->latency_frames = frames / 2; + if ((err = snd_pcm_hw_params (handle, hware)) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot set playback hw_params (%s)\n", + snd_strerror (err)); + goto errend; + } + if ((err = snd_pcm_sw_params_current (handle, sware)) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot get software playback parameters (%s)\n", + snd_strerror (err)); + goto errend; + } + if (snd_pcm_sw_params_set_start_threshold (handle, sware, dev->latency_frames) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot set start threshold\n"); + goto errend; + } + if ((err = snd_pcm_sw_params (handle, sware)) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot set playback sw_params (%s)\n", + snd_strerror (err)); + goto errend; + } + if (quisk_sound_state.verbose_sound) { + snd_pcm_sw_params_get_silence_threshold(sware, &frames); + printf(" play silence threshold %d\n", (int)frames); + snd_pcm_sw_params_get_silence_size(sware, &frames); + printf(" play silence size %d\n", (int)frames); + snd_pcm_sw_params_get_start_threshold(sware, &frames); + printf(" play start threshold %d\n", (int)frames); + } + if ((err = snd_pcm_prepare (handle)) < 0) { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, "Cannot prepare playback interface for use (%s)\n", + snd_strerror (err)); + goto errend; + } + if (quisk_sound_state.verbose_sound) { + buffer_size = period_size = 0; + snd_pcm_get_params (handle, &buffer_size, &period_size); + printf(" Buffer size %d\n Latency frames %d\n Period size %d\n", + (int)buffer_size, dev->latency_frames, (int)period_size); + } + // Success + snd_pcm_hw_params_free (hware); + snd_pcm_sw_params_free (sware); + if (quisk_sound_state.verbose_sound) + printf("*** End playback on alsa device %s %s\n", dev->name, quisk_sound_state.err_msg); + return 0; +errend: + snd_pcm_hw_params_free (hware); + snd_pcm_sw_params_free (sware); + if (quisk_sound_state.verbose_sound) + printf("*** Error end for playback on alsa device %s %s\n", dev->name, quisk_sound_state.err_msg); + return 1; +} + +void quisk_start_sound_alsa (struct sound_dev ** pCapture, struct sound_dev ** pPlayback) +{ + struct sound_dev * pDev; + + memset(bufferz, 0, sizeof(int) * SAMP_BUFFER_SIZE); + is_little_endian = 1; // Test machine byte order + if (*(char *)&is_little_endian == 1) + is_little_endian = 1; + else + is_little_endian = 0; + if (quisk_sound_state.err_msg[0]) + return; // prior error + // Open the alsa playback devices + while (1) { + pDev = *pPlayback++; + if ( ! pDev) + break; + if ( ! pDev->handle && pDev->driver == DEV_DRIVER_ALSA) + if (quisk_open_alsa_playback(pDev)) + return; // error + } + // Open the alsa capture devices and start them + while (1) { + pDev = *pCapture++; + if ( ! pDev) + break; + if ( ! pDev->handle && pDev->driver == DEV_DRIVER_ALSA) { + if (quisk_open_alsa_capture(pDev)) + return; // error + if (pDev->handle) + snd_pcm_start((snd_pcm_t *)pDev->handle); + } + } +} + +void quisk_close_sound_alsa(struct sound_dev ** pCapture, struct sound_dev ** pPlayback) +{ + struct sound_dev * pDev; + + while (*pCapture) { + pDev = *pCapture; + if (pDev->handle && pDev->driver == DEV_DRIVER_ALSA) { + snd_pcm_drop((snd_pcm_t *)pDev->handle); + snd_pcm_close((snd_pcm_t *)pDev->handle); + pDev->handle = NULL; + pDev->driver = DEV_DRIVER_NONE; + } + pCapture++; + } + while (*pPlayback) { + pDev = *pPlayback; + if (pDev->handle && pDev->driver == DEV_DRIVER_ALSA) { + snd_pcm_drop((snd_pcm_t *)pDev->handle); + snd_pcm_close((snd_pcm_t *)pDev->handle); + pDev->handle = NULL; + pDev->driver = DEV_DRIVER_NONE; + } + pPlayback++; + } +} + +void quisk_alsa_mixer_set(char * card_name, int numid, PyObject * value, char * err_msg, int err_size) +// Set card card_name mixer control numid to value for integer, boolean, enum controls. +// If value is a float, interpret value as a decimal fraction of min/max. +{ + int err; + static snd_ctl_t * handle = NULL; + snd_ctl_elem_info_t *info; + snd_ctl_elem_id_t * id; + snd_ctl_elem_value_t * control; + unsigned int idx; + long imin, imax, tmp; + snd_ctl_elem_type_t type; + unsigned int count; + + snd_ctl_elem_info_alloca(&info); + snd_ctl_elem_id_alloca(&id); + snd_ctl_elem_value_alloca(&control); + + err_msg[0] = 0; + + snd_ctl_elem_id_set_interface(id, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_id_set_numid(id, numid); + //snd_ctl_elem_id_set_index(id, index); + //snd_ctl_elem_id_set_device(id, device); + //snd_ctl_elem_id_set_subdevice(id, subdevice); + if ( ! strncmp (card_name, "alsa:", 5)) { // search for the name in info strings + char buf[QUISK_SC_SIZE]; + strMcpy(buf, card_name + 5, QUISK_SC_SIZE); + if ( ! device_list(NULL, SND_PCM_STREAM_CAPTURE, buf)) // check capture and play names + device_list(NULL, SND_PCM_STREAM_PLAYBACK, buf); + buf[4] = 0; // Remove device nuumber + err = snd_ctl_open(&handle, buf, 0); + } + else { // just try to open the name + err = snd_ctl_open(&handle, card_name, 0); + } + if (err < 0) { + snprintf (err_msg, err_size, "Control %s open error: %s\n", card_name, snd_strerror(err)); + return; + } + snd_ctl_elem_info_set_id(info, id); + if ((err = snd_ctl_elem_info(handle, info)) < 0) { + snprintf (err_msg, err_size, "Cannot find the given element from control %s\n", card_name); + return; + } + snd_ctl_elem_info_get_id(info, id); + type = snd_ctl_elem_info_get_type(info); + snd_ctl_elem_value_set_id(control, id); + count = snd_ctl_elem_info_get_count(info); + + for (idx = 0; idx < count; idx++) { + switch (type) { + case SND_CTL_ELEM_TYPE_BOOLEAN: + if (PyObject_IsTrue(value)) + snd_ctl_elem_value_set_boolean(control, idx, 1); + else + snd_ctl_elem_value_set_boolean(control, idx, 0); + break; + case SND_CTL_ELEM_TYPE_INTEGER: + imin = snd_ctl_elem_info_get_min(info); + imax = snd_ctl_elem_info_get_max(info); + if (PyFloat_CheckExact(value)) { + tmp = (long)(imin + (imax - imin) * PyFloat_AsDouble(value) + 0.4); + snd_ctl_elem_value_set_integer(control, idx, tmp); + } + else if(PyInt_Check(value)) { + tmp = PyInt_AsLong(value); + snd_ctl_elem_value_set_integer(control, idx, tmp); + } + else { + snprintf (err_msg, err_size, "Control %s id %d has bad value\n", card_name, numid); + } + break; + case SND_CTL_ELEM_TYPE_INTEGER64: + imin = snd_ctl_elem_info_get_min64(info); + imax = snd_ctl_elem_info_get_max64(info); + if (PyFloat_CheckExact(value)) { + tmp = (long)(imin + (imax - imin) * PyFloat_AsDouble(value) + 0.4); + snd_ctl_elem_value_set_integer64(control, idx, tmp); + } + else if(PyInt_Check(value)) { + tmp = PyInt_AsLong(value); + snd_ctl_elem_value_set_integer64(control, idx, tmp); + } + else { + snprintf (err_msg, err_size, "Control %s id %d has bad value\n", card_name, numid); + } + break; + case SND_CTL_ELEM_TYPE_ENUMERATED: + if(PyInt_Check(value)) { + tmp = PyInt_AsLong(value); + snd_ctl_elem_value_set_enumerated(control, idx, (unsigned int)tmp); + } + else { + snprintf (err_msg, err_size, "Control %s id %d has bad value\n", card_name, numid); + } + break; + default: + snprintf (err_msg, err_size, "Control %s element has unknown type\n", card_name); + break; + } + if ((err = snd_ctl_elem_write(handle, control)) < 0) { + snprintf (err_msg, err_size, "Control %s element write error: %s\n", card_name, snd_strerror(err)); + return; + } + } + snd_ctl_close(handle); + return; +} + +/* The following is based on: */ +// +// Programmer: Craig Stuart Sapp +// Creation Date: Sat May 9 17:50:41 PDT 2009 +// Last Modified: Sat May 9 18:14:05 PDT 2009 +// Filename: alsarawportlist.c +// Syntax: C; ALSA 1.0 +// $Smake: gcc -o %b %f -lasound +// +// Description: Print available input/output MIDI ports using +// using ALSA rawmidi interface. Derived from +// amidi.c (An ALSA 1.0.19 utils program). +// + +#define FRIENDLY_NAME_SIZE 256 +static void midi_in_devices(PyObject * pylist, int just_names) +{ // Return a list of MIDI In devices. + PyObject * pytup; + int card; + snd_ctl_t * ctl; + snd_rawmidi_info_t * info; + char card_name[32]; + char friendly_name[FRIENDLY_NAME_SIZE]; + int device; + const char * name; + int sub, subs; + + card = -1; + snd_rawmidi_info_alloca(&info); + while (1) { // For all cards + if (snd_card_next(&card) < 0 || card < 0) + return; + snprintf(card_name, 32, "hw:%d", card); + if (snd_ctl_open(&ctl, card_name, 0) < 0) + continue; + device = -1; + while (1) { // For all devices + if (snd_ctl_rawmidi_next_device(ctl, &device) < 0 || device < 0) + break; + snd_rawmidi_info_set_device(info, device); + snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_INPUT); + if (snd_ctl_rawmidi_info(ctl, info) == 0) + subs = snd_rawmidi_info_get_subdevices_count(info); + else + subs = 0; + for (sub = 0; sub < subs; sub++) { // For all subdevices + snd_rawmidi_info_set_subdevice(info, sub); + snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_INPUT); + if (snd_ctl_rawmidi_info(ctl, info) == 0) { + name = snd_rawmidi_info_get_subdevice_name(info); + if (name[0] == 0) { + name = snd_rawmidi_info_get_name(info); + if (subs == 1) + strMcpy(friendly_name, name, FRIENDLY_NAME_SIZE); + else + snprintf(friendly_name, FRIENDLY_NAME_SIZE, "%s (%d)", name, sub); + } + else { + strMcpy(friendly_name, name, FRIENDLY_NAME_SIZE); + } + if (just_names) { + PyList_Append(pylist, PyUnicode_DecodeUTF8(friendly_name, strlen(friendly_name), "replace")); + } + else { + pytup = PyTuple_New(2); + PyList_Append(pylist, pytup); + PyTuple_SET_ITEM(pytup, 0, PyUnicode_DecodeUTF8(friendly_name, strlen(friendly_name), "replace")); + snprintf(card_name, 32, "hw:%d,%d,%d", card, device, sub); + PyTuple_SET_ITEM(pytup, 1, PyUnicode_DecodeUTF8(card_name, strlen(card_name), "replace")); + } + } + } + } + } +} + +#define MIDI_MAX 6000 + +PyObject * quisk_alsa_control_midi(PyObject * self, PyObject * args, PyObject * keywds) +{ /* Call with keyword arguments ONLY */ + static char * kwlist[] = {"client", "device", "close_port", "get_event", "midi_cwkey_note", + "get_in_names", "get_in_devices", NULL} ; + int client, close_port, get_event, get_in_names, get_in_devices; + char * device; + static int midi_cwkey_note = -1; + static snd_rawmidi_t * handle_in = NULL; + PyObject * pylist; + unsigned char ch; + static int state = 0; + char midi_chars[MIDI_MAX]; + int midi_length; + + client = close_port = get_event = get_in_names = get_in_devices = -1; + device = NULL; + if (!PyArg_ParseTupleAndKeywords (args, keywds, "|isiiiii", kwlist, + &client, &device, &close_port, &get_event, &midi_cwkey_note, &get_in_names, &get_in_devices)) + return NULL; + if (close_port == 1) { + if (handle_in) + snd_rawmidi_close(handle_in); + handle_in = NULL; + quisk_midi_cwkey = 0; + } + if (get_in_devices == 1) { // return a list of (friendly name, device name) + pylist = PyList_New(0); + midi_in_devices(pylist, 0); + return pylist; + } + if (get_in_names == 1) { // return a list of friendly names + pylist = PyList_New(0); + midi_in_devices(pylist, 1); + return pylist; + } + if (device) { // open port + state = 0; + quisk_midi_cwkey = 0; + if (snd_rawmidi_open(&handle_in, NULL, device, SND_RAWMIDI_NONBLOCK) != 0) { + handle_in = NULL; + printf("Failed to open MIDI device %s\n", device); + } + else { + snd_rawmidi_nonblock(handle_in, 1); + if (quisk_sound_state.verbose_sound) + printf("Open MIDI device %s\n", device); + } + + } + if (get_event == 1 && handle_in) { + midi_length = 0; + while (snd_rawmidi_read(handle_in, &ch, 1) == 1) { + if (midi_length < MIDI_MAX - 1) + midi_chars[midi_length++] = ch; + switch (state) { + case 0: // Wait for a status byte + // Ignore the channel + if (ch & 0x80) { // This is a status byte + if ((ch & 0xF0) == 0x80) // Note Off + state = 1; + else if ((ch & 0xF0) == 0x90) // Note On + state = 2; + } + break; + case 1: // Note Off key number + if (ch == midi_cwkey_note) + quisk_midi_cwkey = 0; + state = 0; + break; + case 2: // Note On key number + if (ch == midi_cwkey_note) + state = 3; + else + state = 0; + break; + case 3: // Note On velocity + if (ch) + quisk_midi_cwkey = 1; + else + quisk_midi_cwkey = 0; + state = 0; + break; + } + } + if (midi_length > 0) + return PyByteArray_FromStringAndSize(midi_chars, midi_length); + } + Py_INCREF (Py_None); + return Py_None; +} +#else // No Alsa available +#include +#include +#include "quisk.h" + +PyObject * quisk_alsa_sound_devices(PyObject * self, PyObject * args) +{ + return quisk_dummy_sound_devices(self, args); +} + +void quisk_start_sound_alsa (struct sound_dev ** pCapture, struct sound_dev ** pPlayback) +{ + struct sound_dev * pDev; + const char * msg = "No driver support for this device"; + + while (*pCapture) { + pDev = *pCapture++; + if (pDev->driver == DEV_DRIVER_ALSA) { + strMcpy(pDev->dev_errmsg, msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", msg); + } + } + while (*pPlayback) { + pDev = *pPlayback++; + if (pDev->driver == DEV_DRIVER_ALSA) { + strMcpy(pDev->dev_errmsg, msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", msg); + } + } +} + +int quisk_read_alsa(struct sound_dev * dev, complex double * cSamples) +{ + return 0; +} + +void quisk_play_alsa(struct sound_dev * playdev, int nSamples, complex double * cSamples, int report_latency, double volume) +{} + +void quisk_close_sound_alsa(struct sound_dev ** pCapture, struct sound_dev ** pPlayback) +{} + +void quisk_alsa_sidetone(struct sound_dev * dev) +{} + +void quisk_alsa_mixer_set(char * card_name, int numid, PyObject * value, char * err_msg, int err_size) +{ + err_msg[0] = 0; +} + +PyObject * quisk_alsa_control_midi(PyObject * self, PyObject * args, PyObject * keywds) +{ + Py_INCREF (Py_None); + return Py_None; +} +#endif diff --git a/sound_directx.c b/sound_directx.c new file mode 100644 index 0000000..605b3cd --- /dev/null +++ b/sound_directx.c @@ -0,0 +1,707 @@ +#ifdef QUISK_HAVE_DIRECTX + +#include +#include +#include +#include "quisk.h" +#include "dsound.h" +//#include +#include +//#include +//#include +#include + + +// This module provides sound card access using Direct Sound + +HRESULT errFound, errOpen; + +extern HWND quisk_mainwin_handle; + +static GUID IEEE = {0x00000003, 0x0000, 0x0010, {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}}; +static GUID PCMM = {0x00000001, 0x0000, 0x0010, {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}}; + +static PyObject * MakePyUnicode(LPCTSTR txt) +{ // return a Python Unicode object + PyObject * py_unicode; + int bytes_txt; + +#ifdef UNICODE + size_t wstr_size = wcslen(txt); + py_unicode = PyUnicode_DecodeUTF16((const char *)txt, wstr_size * sizeof(WCHAR), "replace", NULL); +#else + bytes_txt = strlen(txt); + int wstr_size = MultiByteToWideChar(CP_ACP, 0, txt, bytes_txt, NULL, 0); + LPWSTR wstr = (LPWSTR)malloc(sizeof(WCHAR) * (wstr_size + 1)); + MultiByteToWideChar(CP_ACP, 0, txt, bytes_txt, wstr, wstr_size + 1); + py_unicode = PyUnicode_DecodeUTF16((const char *)wstr, wstr_size * sizeof(WCHAR), "replace", NULL); + free(wstr); +#endif + return py_unicode; +} + +static int match_name(LPCTSTR lpszDesc, const char * name) +{ + int found; + PyObject * py_unicode; + PyObject * py_substring; + Py_ssize_t length, index; + + py_unicode = MakePyUnicode(lpszDesc); + if ( ! py_unicode) + return 0; + py_substring = PyUnicode_DecodeUTF8(name, strlen(name), "replace"); + if ( ! py_substring) { + Py_DECREF(py_unicode); + return 0; + } +#if PY_MAJOR_VERSION >= 3 + if (PyUnicode_READY(py_unicode) == 0) + length = PyUnicode_GET_LENGTH(py_unicode); + else + length = 0; +#else + length = PyUnicode_GET_SIZE(py_unicode); +#endif + if (length <= 0) { + Py_DECREF(py_unicode); + Py_DECREF(py_substring); + return 0; + } + index = PyUnicode_Find(py_unicode, py_substring, 0, length, 1); + if (index >= 0) + found = 1; + else if (index == -1) + found = 0; + else { // error + PyErr_Clear(); + found = 0; + } + Py_DECREF(py_unicode); + Py_DECREF(py_substring); + return found; +} + +static BOOL CALLBACK DSEnumNames(LPGUID lpGUID, LPCTSTR lpszDesc, LPCTSTR lpszDrvName, LPVOID pyseq) +{ + //char * buf = (char *)malloc(2000); + //strcpy (buf, "\xc9vir\xf6n"); + //strcat(buf, (char *)lpszDesc); + //PyObject * py_string = py_str_utf8((LPCTSTR)buf); + PyObject * py_unicode = MakePyUnicode(lpszDesc); + PyList_Append((PyObject *)pyseq, py_unicode); + //free(buf); + Py_DECREF(py_unicode); + return TRUE; +} + +static BOOL CALLBACK DsEnumPlay(LPGUID lpGUID, LPCTSTR lpszDesc, LPCTSTR lpszDrvName, LPVOID dev) +{ // Open the play device if the name is found in the description + LPDIRECTSOUND8 DsDev; + + if (strcmp(lpszDrvName, ((struct sound_dev *)dev)->device_name) == 0 || + match_name(lpszDesc, ((struct sound_dev *)dev)->name)) { + errFound = DS_OK; + errOpen = DirectSoundCreate8(lpGUID, &DsDev, NULL); + if (errOpen == DS_OK) { + ((struct sound_dev *)dev)->handle = DsDev; + } + return FALSE; // Stop iteration + } + else { + return TRUE; + } +} + +static BOOL CALLBACK DsEnumCapture(LPGUID lpGUID, LPCTSTR lpszDesc, LPCTSTR lpszDrvName, LPVOID dev) +{ // Open the capture device if the name is found in the description + LPDIRECTSOUNDCAPTURE8 DsDev; + + //char * buf = (char *)malloc(2000); + //strcpy (buf, "\xc9vir\xf6n"); + //strcat(buf, (char *)lpszDesc); + //PyObject * py_string = py_str_utf8((LPCTSTR)buf); +//QuiskPrintf("EnumCapture .%s. .%s.\n", lpszDesc, lpszDrvName); +//QuiskPrintf(" Capture .%s. .%s.\n", ((struct sound_dev *)dev)->name, ((struct sound_dev *)dev)->device_name); + if (strcmp(lpszDrvName, ((struct sound_dev *)dev)->device_name) == 0 || + match_name(lpszDesc, ((struct sound_dev *)dev)->name)) { + errFound = DS_OK; + errOpen = DirectSoundCaptureCreate8(lpGUID, &DsDev, NULL); + if (errOpen == DS_OK) + ((struct sound_dev *)dev)->handle = DsDev; + return FALSE; // Stop iteration + } + else { + return TRUE; + } +} + +static void MakeWFext(int use_new, int use_float, struct sound_dev * dev, WAVEFORMATEXTENSIBLE * pwfex) +{ // fill in a WAVEFORMATEXTENSIBLE structure + if (use_float) { + dev->sample_bytes = 4; + dev->sound_format = Float32; + } + if (use_new) { + pwfex->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + pwfex->Format.cbSize = 22; + pwfex->Samples.wValidBitsPerSample = dev->sample_bytes * 8; + if (dev->num_channels == 1) + pwfex->dwChannelMask = SPEAKER_FRONT_LEFT; + else + pwfex->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; + if (use_float) { + pwfex->SubFormat = IEEE; + dev->use_float = 1; + } + else { + pwfex->SubFormat = PCMM; + dev->use_float = 0; + } + } + else { + pwfex->Format.cbSize = 0; + if (use_float) { + pwfex->Format.wFormatTag = 0x03; //WAVE_FORMAT_IEEE; + dev->use_float = 1; + } + else { + pwfex->Format.wFormatTag = WAVE_FORMAT_PCM; + dev->use_float = 0; + } + } + pwfex->Format.nChannels = dev->num_channels; + pwfex->Format.nSamplesPerSec = dev->sample_rate; + pwfex->Format.nAvgBytesPerSec = dev->num_channels * dev->sample_rate * dev->sample_bytes; + dev->play_buf_bytes = pwfex->Format.nAvgBytesPerSec * quisk_sound_state.latency_millisecs / 1000 * 2; + dev->play_buf_size = dev->play_buf_bytes / (dev->num_channels * dev->sample_bytes); + pwfex->Format.nBlockAlign = dev->num_channels * dev->sample_bytes; + pwfex->Format.wBitsPerSample = dev->sample_bytes * 8; +} + +static int quisk_open_capture(struct sound_dev * dev) +{ // Open the soundcard for capture. Return non-zero for error. + LPDIRECTSOUNDCAPTUREBUFFER ptBuf; + DSCBUFFERDESC dscbd; + HRESULT hr; + WAVEFORMATEXTENSIBLE wfex; + + if (quisk_sound_state.verbose_sound) + QuiskPrintf("Opening capture device %s\n Name %s\n Device name %s\n", dev->stream_description, dev->name, dev->device_name); + dev->handle = NULL; + dev->buffer = NULL; + dev->started = 0; + dev->dataPos = 0; + dev->portaudio_index = -1; + if ( ! dev->name[0]) // Check for null play name; not an error + return 0; + errFound = ~DS_OK; + DirectSoundCaptureEnumerate((LPDSENUMCALLBACK)DsEnumCapture, dev); + if (errFound != DS_OK) { + snprintf (dev->dev_errmsg, QUISK_SC_SIZE, + "DirectSound capture device name %s not found", dev->name); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", dev->dev_errmsg); + return 1; + } + if (errOpen != DS_OK) { + snprintf (dev->dev_errmsg, QUISK_SC_SIZE, + "DirectSound capture device %s open failed", dev->name); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", dev->dev_errmsg); + return 1; + } + dev->sample_bytes = 4; + dev->sound_format = Int32; + MakeWFext (1, 0, dev, &wfex); // fill in wfex + memset(&dscbd, 0, sizeof(DSCBUFFERDESC)); + dscbd.dwSize = sizeof(DSCBUFFERDESC); + dscbd.dwFlags = 0; + dscbd.dwBufferBytes = dev->play_buf_bytes; + dscbd.lpwfxFormat = (WAVEFORMATEX *)&wfex; + hr = IDirectSoundCapture_CreateCaptureBuffer( + (LPDIRECTSOUNDCAPTURE8)dev->handle, &dscbd, &ptBuf, NULL); + if (hr == DS_OK) { + dev->buffer = ptBuf; + } + else { + snprintf (dev->dev_errmsg, QUISK_SC_SIZE, + "DirectSound capture device %s buffer create failed (0x%lX)", dev->name, hr); + return 1; + } + ptBuf = (LPDIRECTSOUNDCAPTUREBUFFER)dev->buffer; + hr = IDirectSoundCaptureBuffer8_Start(ptBuf, DSCBSTART_LOOPING); + if (hr != DS_OK) { + if (quisk_sound_state.verbose_sound) + QuiskPrintf("Capture start error 0x%lX", hr); + snprintf (dev->dev_errmsg, QUISK_SC_SIZE, + "DirectSound capture device %s capture start failed", dev->name); + return 1; + } + if (quisk_sound_state.verbose_sound) { + QuiskPrintf(" Sample rate %d\n Channel_I %d\n Channel_Q %d\n", + dev->sample_rate, dev->channel_I, dev->channel_Q); + QuiskPrintf(" Number of channels %d, bytes per sample %d\n", dev->num_channels, dev->sample_bytes); + QuiskPrintf("Created capture buffer size %d bytes for device %s, descr %s\n", + dev->play_buf_bytes, dev->name, dev->stream_description); + } + return 0; +} + +static int quisk_open_playback(struct sound_dev * dev) +{ // Open the soundcard for playback. Return non-zero for error. + LPDIRECTSOUNDBUFFER ptBuf; + WAVEFORMATEXTENSIBLE wfex; + DSBUFFERDESC dsbdesc; + HRESULT hr; + + if (quisk_sound_state.verbose_sound) + QuiskPrintf("Opening playback device %s\n Name %s\n Device name %s\n", dev->stream_description, dev->name, dev->device_name); + dev->handle = NULL; + dev->buffer = NULL; + dev->started = 0; + dev->dataPos = 0; + dev->portaudio_index = -1; + dev->sample_bytes = 2; + dev->sound_format = Int16; + if ( ! dev->name[0]) // Check for null play name; not an error + return 0; + errFound = ~DS_OK; + DirectSoundEnumerate((LPDSENUMCALLBACK)DsEnumPlay, dev); + if (errFound != DS_OK) { + snprintf (dev->dev_errmsg, QUISK_SC_SIZE, + "DirectSound play device name %s not found", dev->name); + return 1; + } + if (errOpen != DS_OK) { + snprintf (dev->dev_errmsg, QUISK_SC_SIZE, + "DirectSound play device %s open failed", dev->name); + return 1; + } + hr = IDirectSound_SetCooperativeLevel ((LPDIRECTSOUND8)dev->handle, quisk_mainwin_handle, DSSCL_PRIORITY); + if (hr != DS_OK) { + snprintf (dev->dev_errmsg, QUISK_SC_SIZE, + "DirectSound play device %s cooperative level failed", dev->name); + return 1; + } + dev->sample_bytes = 4; + dev->sound_format = Int32; + MakeWFext (1, 0, dev, &wfex); // fill in wfex + memset(&dsbdesc, 0, sizeof(DSBUFFERDESC)); + dsbdesc.dwSize = sizeof(DSBUFFERDESC); + dsbdesc.dwFlags = DSBCAPS_GETCURRENTPOSITION2|DSBCAPS_GLOBALFOCUS; + dsbdesc.dwBufferBytes = dev->play_buf_bytes; + dsbdesc.lpwfxFormat = (LPWAVEFORMATEX)&wfex; + hr = IDirectSound_CreateSoundBuffer( + (LPDIRECTSOUND8)dev->handle, &dsbdesc, &ptBuf, NULL); + if (hr == DS_OK) { + dev->buffer = ptBuf; + } + else { + snprintf (dev->dev_errmsg, QUISK_SC_SIZE, + "DirectSound play device %s buffer create failed (0x%X)", dev->name, (unsigned int)hr); + return 1; + } + if (quisk_sound_state.verbose_sound) { + QuiskPrintf(" Sample rate %d\n Channel_I %d\n Channel_Q %d\n", + dev->sample_rate, dev->channel_I, dev->channel_Q); + QuiskPrintf(" Number of channels %d, bytes per sample %d\n", dev->num_channels, dev->sample_bytes); + QuiskPrintf("Created play buffer size %d bytes for device %s, descr %s\n", + dev->play_buf_bytes, dev->name, dev->stream_description); + } + return 0; +} + +PyObject * quisk_directx_sound_devices(PyObject * self, PyObject * args) +{ // Return a list of DirectSound device names + PyObject * pylist, * pycapt, * pyplay; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + + // Each pycapt and pyplay is a device name + pylist = PyList_New(0); // list [pycapt, pyplay] + pycapt = PyList_New(0); // list of capture devices + pyplay = PyList_New(0); // list of play devices + PyList_Append(pylist, pycapt); + PyList_Append(pylist, pyplay); + DirectSoundCaptureEnumerate((LPDSENUMCALLBACK)DSEnumNames, pycapt); + DirectSoundEnumerate((LPDSENUMCALLBACK)DSEnumNames, pyplay); + return pylist; +} + +void quisk_start_sound_directx (struct sound_dev ** pCapture, struct sound_dev ** pPlayback) +{ + struct sound_dev * pDev; + + // DirectX must open the playback device before the (same) capture device + while (1) { + pDev = *pPlayback++; + if ( ! pDev) + break; + if ( ! pDev->dev_errmsg[0] && pDev->driver == DEV_DRIVER_DIRECTX) + quisk_open_playback(pDev); + } + while (1) { + pDev = *pCapture++; + if ( ! pDev) + break; + if ( ! pDev->dev_errmsg[0] && pDev->driver == DEV_DRIVER_DIRECTX) + quisk_open_capture(pDev); + } +} + +void quisk_close_sound_directx(struct sound_dev ** pCapture, struct sound_dev ** pPlayback) +{ + struct sound_dev * pDev; + + while (*pPlayback) { + pDev = *pPlayback; + if (pDev->buffer) + IDirectSoundBuffer8_Stop((LPDIRECTSOUNDBUFFER)pDev->buffer); + pDev->handle = NULL; + pPlayback++; + } + while (*pCapture) { + pDev = *pCapture; + if (pDev->buffer) + IDirectSoundCaptureBuffer_Stop((LPDIRECTSOUNDCAPTUREBUFFER)pDev->buffer); + pDev->handle = NULL; + pCapture++; + } +} + +int quisk_read_directx(struct sound_dev * dev, complex double * cSamples) +{ // cSamples can be NULL to discard samples + LPDIRECTSOUNDCAPTUREBUFFER ptBuf = (LPDIRECTSOUNDCAPTUREBUFFER)dev->buffer; + HRESULT hr; + DWORD readPos, captPos; + LPVOID pt1, pt2; + DWORD n1, n2; + int16_t si, sq, * pts; + float fi, fq, * ptf; + int32_t li, lq, * ptl; + int ii, qq, nSamples; + int bytes, frames, bytes_per_frame; + + if ( ! dev->handle || ! dev->buffer) + return 0; + + bytes_per_frame = dev->num_channels * dev->sample_bytes; + hr = IDirectSoundCaptureBuffer8_GetCurrentPosition(ptBuf, &captPos, &readPos); + if (hr != DS_OK) { +#if DEBUG_IO + QuiskPrintf ("Get CurrentPosition error 0x%lX\n", hr); +#endif + dev->dev_error++; + return 0; + } + if ( ! dev->started) { + dev->started = 1; + dev->dataPos = readPos; + } + if (readPos >= dev->dataPos) + bytes = readPos - dev->dataPos; + else + bytes = readPos - dev->dataPos + dev->play_buf_bytes; + frames = bytes / bytes_per_frame; // frames available to read + dev->dev_latency = frames; +//if (dev->dev_index == t_Capture) QuiskPrintf("Read %d frames\n", frames); + if ( ! bytes) + return 0; + if (IDirectSoundCaptureBuffer8_Lock(ptBuf, dev->dataPos, bytes, &pt1, &n1, &pt2, &n2, 0) != DS_OK) { + dev->dev_error++; +#if DEBUG_IO + QuiskPrintf ("DirecctX capture lock error bytes %d\n", bytes); +#endif + return 0; + } +//QuiskPrintf ("%d %d %d %d\n", dev->channel_I, dev->channel_Q, bytes_per_frame, dev->num_channels); +#if DEBUG_IO + QuiskPrintf("%s read %4d bytes %4d frames from %9d to (%9lu %9lu) diff %9lu\n", dev->name, + bytes, frames, dev->dataPos, readPos, captPos, captPos - readPos); +#endif +#if DEBUG_IO + if (bytes != n1 + n2) + QuiskPrintf ("Lock not equal to bytes\n"); +#endif + dev->dataPos += bytes; + if (dev->dataPos >= dev->play_buf_bytes) + dev->dataPos -= dev->play_buf_bytes; + nSamples = 0; + switch (dev->sound_format) { + case Int16: + pts = (short *)pt1; + frames = (n1 + n2) / bytes_per_frame; + bytes = 0; + while (frames) { + si = pts[dev->channel_I]; + sq = pts[dev->channel_Q]; + pts += dev->num_channels; + if (si >= CLIP16 || si <= -CLIP16) + dev->overrange++; // assume overrange returns max int + if (sq >= CLIP16 || sq <= -CLIP16) + dev->overrange++; + ii = si << 16; + qq = sq << 16; + if (nSamples < SAMP_BUFFER_SIZE * 8 / 10) { + if (cSamples) + cSamples[nSamples] = ii + I * qq; + nSamples++; + } + bytes += bytes_per_frame; + frames--; + if (bytes == n1) + pts = (short *)pt2; + } + break; + case Int32: + ptl = (int *)pt1; + frames = (n1 + n2) / bytes_per_frame; + bytes = 0; + while (frames) { + li = ptl[dev->channel_I]; + lq = ptl[dev->channel_Q]; + ptl += dev->num_channels; + if (li >= CLIP32 || li <= -CLIP32) + dev->overrange++; // assume overrange returns max int + if (lq >= CLIP32 || lq <= -CLIP32) + dev->overrange++; + if (nSamples < SAMP_BUFFER_SIZE * 8 / 10) { + if (cSamples) + cSamples[nSamples] = li + I * lq; + nSamples++; + } + bytes += bytes_per_frame; + frames--; + if (bytes == n1) + ptl = (int *)pt2; + } + break; + case Float32: // use IEEE float + ptf = (float *)pt1; + frames = (n1 + n2) / bytes_per_frame; + bytes = 0; + while (frames) { + fi = ptf[dev->channel_I]; + fq = ptf[dev->channel_Q]; + ptf += dev->num_channels; + if (fabsf(fi) >= 1.0 || fabsf(fq) >= 1.0) + dev->overrange++; // assume overrange returns maximum + if (nSamples < SAMP_BUFFER_SIZE * 8 / 10) { + if (cSamples) + cSamples[nSamples] = (fi + I * fq) * 16777215; + nSamples++; + } + bytes += bytes_per_frame; + frames--; + if (bytes == n1) { + ptf = (float *)pt2; + } + } + break; + case Int24: + break; + } + IDirectSoundCaptureBuffer8_Unlock(ptBuf, pt1, n1, pt2, n2); + return nSamples; +} + +void quisk_play_directx(struct sound_dev * dev, int nSamples, + complex double * cSamples, int report_latency, double volume) +{ + LPDIRECTSOUNDBUFFER ptBuf = (LPDIRECTSOUNDBUFFER)dev->buffer; + DWORD playPos, writePos; // hardware index into buffer + LPVOID pt1, pt2; + DWORD n1, n2; + short * pts; + float * ptf; + int * ptl; // int must be 32 bits + int n, unavail, frames, bytes, pass, bytes_per_frame; + float buffer_fill; + + if ( ! dev->handle || ! dev->buffer) + return; + + bytes_per_frame = dev->num_channels * dev->sample_bytes; + // Note: writePos moves ahead with playPos; it is not associated with write activity + if (IDirectSoundBuffer8_GetCurrentPosition(ptBuf, &playPos, &writePos) != DS_OK) { + if (quisk_sound_state.verbose_sound) + QuiskPrintf ("Bad GetCurrentPosition for %s\n", dev->stream_description); + quisk_sound_state.write_error++; + dev->dev_error++; + playPos = writePos = 0; + } + unavail = (int)writePos - (int)playPos; // Must not write to this region + if (unavail < 0) + unavail += dev->play_buf_bytes; + dev->play_delay = dev->dataPos - playPos; // includes unavail + if (dev->play_delay < 0) + dev->play_delay += dev->play_buf_bytes; + dev->cr_average_fill += ((double)(dev->play_delay - unavail) / bytes_per_frame + nSamples / 2) / (2 * dev->latency_frames); + dev->cr_average_count++; + // buffer_fill is (the writable play samples plus the current samples) / (2 * latency_frames) + // so 0.5 filled represents the target level of dev->latency_frames + buffer_fill = ((float)(dev->play_delay - unavail) / bytes_per_frame + nSamples) / (2 * dev->latency_frames); + switch(dev->started) { + case 0: // Starting state; wait for buffer to fill before starting play + if (buffer_fill >= 0.5) { + IDirectSoundBuffer8_Play (ptBuf, 0, 0, DSBPLAY_LOOPING); + dev->started = 1; + if (quisk_sound_state.verbose_sound) + QuiskPrintf ("Start DirectX play at dev->latency_frames %d\n", dev->latency_frames); + } + break; + case 1: // Normal run state + // Measure the space available to write samples + frames = (dev->play_buf_bytes - dev->play_delay - unavail) / bytes_per_frame; + // Check for underrun + n = unavail / bytes_per_frame + dev->latency_frames * 2 / 10 - nSamples; // minimum frames + if (dev->dev_latency < n) { + quisk_sound_state.underrun_error++; + dev->dev_underrun++; + if (quisk_sound_state.verbose_sound) + QuiskPrintf ("Underrun error for %s\n", dev->stream_description); + n += dev->latency_frames * 2 / 10; + while (n-- > 0) + cSamples[nSamples++] = 0; // add zero samples + } + // Check if play buffer is too full + else if (dev->dev_latency > dev->latency_frames * 18 / 10 || nSamples >= frames) { + quisk_sound_state.write_error++; + dev->dev_error++; + if (quisk_sound_state.verbose_sound) + QuiskPrintf ("Buffer too full for %s\n", dev->stream_description); + nSamples = 0; + dev->started = 2; + } + break; + case 2: // Buffer is too full; wait for it to drain + if (buffer_fill <= 0.5) { + dev->started = 1; + if (quisk_sound_state.verbose_sound) + QuiskPrintf("Resume adding samples for %s\n", dev->stream_description); + } + else { + nSamples = 0; + } + break; + } + bytes = nSamples * bytes_per_frame; + if (bytes <= 0) + return; + // write our data bytes at our data position dataPos + if (IDirectSoundBuffer8_Lock(ptBuf, dev->dataPos, bytes, &pt1, &n1, &pt2, &n2, 0) != DS_OK) { + if (quisk_sound_state.verbose_sound) + QuiskPrintf ("DirectX play lock error for %s\n", dev->stream_description); + quisk_sound_state.write_error++; + dev->dev_error++; + return; + } + pass = 0; + n = 0; +//if (dev->dev_index == t_Playback) QuiskPrintf("Buffer fill %.3f%% bytes %d for %s\n", buffer_fill * 100, bytes, dev->stream_description); + switch (dev->sound_format) { + case Int16: + pts = (short *)pt1; // Start writing at pt1 + frames = n1 / bytes_per_frame; + for (n = 0; n < nSamples && pass < 2; n++) { + pts[dev->channel_I] = (short)(volume * creal(cSamples[n]) / 65536); + pts[dev->channel_Q] = (short)(volume * cimag(cSamples[n]) / 65536); + pts += dev->num_channels; + if (--frames <= 0) { + pass++; + // change to pt2 + pts = (short *)pt2; + frames = n2 / bytes_per_frame; + } + } + break; + case Int32: + ptl = (int *)pt1; // Start writing at pt1 + frames = n1 / bytes_per_frame; + for (n = 0; n < nSamples && pass < 2; n++) { + ptl[dev->channel_I] = (int)(volume * creal(cSamples[n])); + ptl[dev->channel_Q] = (int)(volume * cimag(cSamples[n])); + ptl += dev->num_channels; + if (--frames <= 0) { + pass++; + // change to pt2 + ptl = (int *)pt2; + frames = n2 / bytes_per_frame; + } + } + break; + case Float32: // use IEEE float + ptf = (float *)pt1; // Start writing at pt1 + frames = n1 / bytes_per_frame; + for (n = 0; n < nSamples && pass < 2; n++) { + ptf[dev->channel_I] = (volume * creal(cSamples[n]) / CLIP32); + ptf[dev->channel_Q] = (volume * cimag(cSamples[n]) / CLIP32); + ptf += dev->num_channels; + if (--frames <= 0) { + pass++; + // change to pt2 + ptf = (float *)pt2; + frames = n2 / bytes_per_frame; + } + } + break; + case Int24: + break; + } + IDirectSoundBuffer8_Unlock(ptBuf, pt1, n1, pt2, n2); + dev->dataPos += bytes; // update data write position + if (dev->dataPos >= dev->play_buf_bytes) + dev->dataPos -= dev->play_buf_bytes; + dev->dev_latency = dev->play_delay / bytes_per_frame; // frames in buffer available to play + if (report_latency) // Report latency for main playback device + quisk_sound_state.latencyPlay = dev->dev_latency; + dev->play_delay += bytes; // bytes available to play + dev->old_play_delay = dev->play_delay; +} +#else // No directx available +#include +#include +#include "quisk.h" + +PyObject * quisk_directx_sound_devices(PyObject * self, PyObject * args) +{ + return quisk_dummy_sound_devices(self, args); +} + +void quisk_start_sound_directx (struct sound_dev ** pCapture, struct sound_dev ** pPlayback) +{ + struct sound_dev * pDev; + const char * msg = "No driver support for this device"; + + while (*pCapture) { + pDev = *pCapture++; + if (pDev->driver == DEV_DRIVER_DIRECTX) { + strMcpy(pDev->dev_errmsg, msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", msg); + } + } + while (*pPlayback) { + pDev = *pPlayback++; + if (pDev->driver == DEV_DRIVER_DIRECTX) { + strMcpy(pDev->dev_errmsg, msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", msg); + } + } +} + +int quisk_read_directx(struct sound_dev * dev, complex double * cSamples) +{ + return 0; +} + +void quisk_play_directx(struct sound_dev * dev, int nSamples, complex double * cSamples, int report_latency, double volume) +{} + +void quisk_close_sound_directx(struct sound_dev ** pCapture, struct sound_dev ** pPlayback) +{} + +#endif diff --git a/sound_portaudio.c b/sound_portaudio.c new file mode 100644 index 0000000..466960f --- /dev/null +++ b/sound_portaudio.c @@ -0,0 +1,547 @@ +/* + * This module provides sound access for QUISK using the portaudio library. +*/ +#ifdef QUISK_HAVE_PORTAUDIO + +#include +#include +#include +#include +#include +#include +#include "quisk.h" + +/* + The sample rate is in frames per second. Each frame has a number of channels, + and each channel has a sample of size sample_bytes. The channels are interleaved: + (channel0, channel1), (channel0, channel1), ... +*/ + +extern struct sound_conf quisk_sound_state; // Current sound status + +static float fbuffer[SAMP_BUFFER_SIZE]; // Buffer for float32 samples from sound + +int quisk_read_portaudio(struct sound_dev * dev, complex double * cSamples) +{ // cSamples can be NULL to discard samples + // Read sound samples from the soundcard. + // Samples are converted to 32 bits with a range of +/- CLIP32 and placed into cSamples. + int i; + long avail; + int nSamples; + PaError error; + float fi, fq; + + if (!dev->handle) + return -1; + + avail = Pa_GetStreamReadAvailable((PaStream * )dev->handle); + dev->dev_latency = avail; + if (dev->read_frames == 0) { // non-blocking: read available frames + if (avail > SAMP_BUFFER_SIZE / dev->num_channels) // limit read request to buffer size + avail = SAMP_BUFFER_SIZE / dev->num_channels; + } + else { // size of read request + avail = dev->read_frames; + } + error = Pa_ReadStream ((PaStream * )dev->handle, fbuffer, avail); + if (error != paNoError) { + dev->dev_error++; + } + nSamples = 0; + for (i = 0; avail; i += dev->num_channels, avail--) { + fi = fbuffer[i + dev->channel_I]; + fq = fbuffer[i + dev->channel_Q]; + if (fi >= 1.0 || fi <= -1.0) + dev->overrange++; // assume overrange returns max int + if (fq >= 1.0 || fq <= -1.0) + dev->overrange++; + if (cSamples) + cSamples[nSamples] = (fi + I * fq) * CLIP32; + nSamples++; + if (nSamples > SAMP_BUFFER_SIZE * 8 / 10) + break; + + } + return nSamples; +} + +void quisk_play_portaudio(struct sound_dev * playdev, int nSamples, complex double * cSamples, + int report_latency, double volume) +{ // play the samples; write them to the portaudio soundcard + int i, n; + long delay, write_available; + float fi, fq; + PaError error; + + if (!playdev->handle || nSamples <= 0) + return; + + // "delay" is the number of samples left in the play buffer + write_available = Pa_GetStreamWriteAvailable(playdev->handle); + delay = playdev->play_buf_size - write_available; + playdev->cr_average_fill += (double)(delay + nSamples / 2) / (playdev->latency_frames * 2); + playdev->cr_average_count++; + playdev->dev_latency = delay; + if (report_latency) { // Report for main playback device + quisk_sound_state.latencyPlay = delay; + } + switch(playdev->started) { + case 0: // Starting state + playdev->started = 1; + nSamples = playdev->latency_frames - delay; + for (i = 0; i < nSamples; i++) + cSamples[i] = 0; + break; + case 1: // Normal run state + // Check if play buffer is too full. + if (nSamples > write_available) { + quisk_sound_state.write_error++; + playdev->dev_error++; + if (quisk_sound_state.verbose_sound) + QuiskPrintf ("Buffer too full for %s\n", playdev->stream_description); + nSamples = 0; + playdev->started = 2; + } + break; + case 2: // Buffer is too full; wait for it to drain + if (delay < playdev->latency_frames) { + playdev->started = 1; + if (quisk_sound_state.verbose_sound) + QuiskPrintf("Resume adding samples for %s\n", playdev->stream_description); + } + else { + nSamples = 0; + } + break; + } + if (nSamples <= 0) + return; + for (i = 0, n = 0; n < nSamples; i += playdev->num_channels, n++) { + fi = volume * creal(cSamples[n]); + fq = volume * cimag(cSamples[n]); + fbuffer[i + playdev->channel_I] = fi / CLIP32; + fbuffer[i + playdev->channel_Q] = fq / CLIP32; + } + error = Pa_WriteStream ((PaStream * )playdev->handle, fbuffer, nSamples); + if (error == paNoError) + ; + else if (error == paOutputUnderflowed) { + if (quisk_sound_state.verbose_sound) + printf("Underrun for %s\n", playdev->stream_description); + quisk_sound_state.underrun_error++; + playdev->dev_underrun++; + nSamples = playdev->latency_frames - nSamples; // add zeros to fill back up to half full + if (nSamples > 0) { + for (i = 0, n = 0; n < nSamples; i += playdev->num_channels, n++) { + fbuffer[i + playdev->channel_I] = 0; + fbuffer[i + playdev->channel_Q] = 0; + } + Pa_WriteStream ((PaStream * )playdev->handle, fbuffer, nSamples); + } + } + else { + quisk_sound_state.write_error++; + playdev->dev_error++; +#if DEBUG_IO + printf ("Portaudio play error: %s\n", Pa_GetErrorText(error)); +#endif + } +} + +static void info_portaudio (struct sound_dev * cDev, struct sound_dev * pDev) +{ // Return information about the device + const PaDeviceInfo * info; + PaStreamParameters params; + int index, rate; + + if (cDev) + index = cDev->portaudio_index; + else if (pDev) + index = pDev->portaudio_index; + else + return; + info = Pa_GetDeviceInfo(index); + if ( ! info) + return; + + params.device = index; + params.channelCount = 1; + params.sampleFormat = paFloat32; + params.suggestedLatency = 0.10; + params.hostApiSpecificStreamInfo = NULL; + + if (cDev) { + cDev->chan_min = 1; + cDev->chan_max = info->maxInputChannels; + cDev->rate_min = cDev->rate_max = 0; + cDev->portaudio_latency = info->defaultHighInputLatency; +#if DEBUG_IO + printf ("Capture latency low %lf, high %lf\n", + info->defaultLowInputLatency, info->defaultHighInputLatency); +#endif + for (rate = 8000; rate <= 384000; rate += 8000) { + if (Pa_IsFormatSupported(¶ms, NULL, rate) == paFormatIsSupported) { + if (cDev->rate_min == 0) + cDev->rate_min = rate; + cDev->rate_max = rate; + } + } + } + + if (pDev) { + pDev->chan_min = 1; + pDev->chan_max = info->maxOutputChannels; + pDev->rate_min = pDev->rate_max = 0; + pDev->portaudio_latency = quisk_sound_state.latency_millisecs / 1000.0 * 2.0; + if (pDev->portaudio_latency < info->defaultHighOutputLatency) + pDev->portaudio_latency = info->defaultHighOutputLatency; +#if DEBUG_IO + printf ("Play latency low %lf, high %lf\n", + info->defaultLowOutputLatency, info->defaultHighOutputLatency); +#endif + for (rate = 8000; rate <= 384000; rate += 8000) { + if (Pa_IsFormatSupported(¶ms, NULL, rate) == paFormatIsSupported) { + if (pDev->rate_min == 0) + pDev->rate_min = rate; + pDev->rate_max = rate; + } + } + } +} + +static int quisk_pa_name2index (struct sound_dev * dev, int is_capture) +{ // Based on the device name, set the portaudio index, or -1. + // Return non-zero for error. Not a portaudio device is not an error. + const PaDeviceInfo * pInfo; + int i, count; + + if (strncmp (dev->name, "portaudio", 9)) { + dev->portaudio_index = -1; // Name does not start with "portaudio" + return 0; // Not a portaudio device, not an error + } + if ( ! strcmp (dev->name, "portaudiodefault")) { + if (is_capture) // Fill in the default device index + dev->portaudio_index = Pa_GetDefaultInputDevice(); + else + dev->portaudio_index = Pa_GetDefaultOutputDevice(); + strMcpy (dev->msg1, "Using default portaudio device", QUISK_SC_SIZE); + return 0; + } + if ( ! strncmp (dev->name, "portaudio#", 10)) { // Integer index follows "#" + dev->portaudio_index = i = atoi(dev->name + 10); + pInfo = Pa_GetDeviceInfo(i); + if (pInfo) { + snprintf (dev->msg1, QUISK_SC_SIZE, "PortAudio device %s", pInfo->name); + return 0; + } + else { + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, + "Error: Can not find portaudio device number %s", dev->name + 10); + strMcpy(dev->dev_errmsg, quisk_sound_state.err_msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + printf("%s\n", quisk_sound_state.err_msg); + } + return 1; + } + if ( ! strncmp (dev->name, "portaudio:", 10)) { + dev->portaudio_index = -1; + count = Pa_GetDeviceCount(); // Search for string in device name + for (i = 0; i < count; i++) { + pInfo = Pa_GetDeviceInfo(i); + if (pInfo && strstr(pInfo->name, dev->name + 10)) { + dev->portaudio_index = i; + snprintf (dev->msg1, QUISK_SC_SIZE, "PortAudio device %s", pInfo->name); + break; + } + } + if (dev->portaudio_index == -1) { // Error + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, + "Error: Can not find portaudio device named %s", dev->name + 10); + strMcpy(dev->dev_errmsg, quisk_sound_state.err_msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + printf("%s\n", quisk_sound_state.err_msg); + return 1; + } + return 0; + } + snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE, + "Error: Did not recognize portaudio device %.90s", dev->name); + strMcpy(dev->dev_errmsg, quisk_sound_state.err_msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + printf("%s\n", quisk_sound_state.err_msg); + return 1; +} + +static int quisk_open_portaudio (struct sound_dev * cDev, struct sound_dev * pDev) +{ // Open the portaudio soundcard for capture on cDev and playback on pDev (or NULL). + // Return non-zero for error. + PaStreamParameters cParams, pParams; + PaError error; + PaStream * hndl; + const PaStreamInfo * ptStreamInfo; + char * indent; + + info_portaudio (cDev, pDev); + + if (pDev && cDev && pDev->sample_rate != cDev->sample_rate) { + strMcpy(quisk_sound_state.err_msg, "Capture and Play sample rates must be equal.", QUISK_SC_SIZE); + strMcpy(cDev->dev_errmsg, quisk_sound_state.err_msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + printf("%s\n", quisk_sound_state.err_msg); + return 1; + } + + cParams.sampleFormat = paFloat32; + pParams.sampleFormat = paFloat32; + cParams.hostApiSpecificStreamInfo = NULL; + pParams.hostApiSpecificStreamInfo = NULL; + + if (cDev && pDev) { + indent = " "; + if (quisk_sound_state.verbose_sound) + printf("Open duplex PortAudio device\n"); + } + else { + indent = ""; + } + if (cDev) { + if (quisk_sound_state.verbose_sound) + printf("%sOpen PortAudio capture device index %d name %s for %s\n", + indent, cDev->portaudio_index, cDev->name, cDev->stream_description); + cDev->handle = NULL; + cParams.device = cDev->portaudio_index; + cParams.channelCount = cDev->num_channels; + cParams.suggestedLatency = cDev->portaudio_latency; + } + + if (pDev) { + if (quisk_sound_state.verbose_sound) + printf("%sOpen PortAudio play device index %d name %s for %s\n", + indent, pDev->portaudio_index, pDev->name, pDev->stream_description); + pDev->handle = NULL; + pParams.device = pDev->portaudio_index; + pParams.channelCount = pDev->num_channels; + pParams.suggestedLatency = pDev->portaudio_latency; + } + + if (cDev && pDev) { + error = Pa_OpenStream (&hndl, &cParams, &pParams, + (double)cDev->sample_rate, cDev->read_frames, 0, NULL, NULL); + pDev->handle = cDev->handle = (void *)hndl; + if (error != paNoError) { + strMcpy(quisk_sound_state.err_msg, Pa_GetErrorText(error), QUISK_SC_SIZE); + strMcpy(cDev->dev_errmsg, quisk_sound_state.err_msg, QUISK_SC_SIZE); + strMcpy(pDev->dev_errmsg, quisk_sound_state.err_msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + printf("%s\n", quisk_sound_state.err_msg); + } + } + else if (cDev) { + error = Pa_OpenStream (&hndl, &cParams, NULL, + (double)cDev->sample_rate, cDev->read_frames, 0, NULL, NULL); + cDev->handle = (void *)hndl; + if (error != paNoError) { + strMcpy(quisk_sound_state.err_msg, Pa_GetErrorText(error), QUISK_SC_SIZE); + strMcpy(cDev->dev_errmsg, quisk_sound_state.err_msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + printf("%s\n", quisk_sound_state.err_msg); + } + } + else if (pDev) { + error = Pa_OpenStream (&hndl, NULL, &pParams, + (double)pDev->sample_rate, 0, 0, NULL, NULL); + pDev->handle = (void *)hndl; + if (error != paNoError) { + strMcpy(quisk_sound_state.err_msg, Pa_GetErrorText(error), QUISK_SC_SIZE); + strMcpy(pDev->dev_errmsg, quisk_sound_state.err_msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + printf("%s\n", quisk_sound_state.err_msg); + } + } + else { + error = paNoError; + } + if (pDev) { + ptStreamInfo = Pa_GetStreamInfo(pDev->handle); + pDev->play_buf_size = (int)(ptStreamInfo->outputLatency * pDev->sample_rate + 0.5); + } + if (pDev && quisk_sound_state.verbose_sound) { + printf ("%s: portaudio play_buf_size %d\n", pDev->stream_description, pDev->play_buf_size); + printf ("%s: portaudio latency_frames %d\n", pDev->stream_description, pDev->latency_frames); + printf ("%s: portaudio portaudio_latency %lf\n", pDev->stream_description, pDev->portaudio_latency); + printf ("%s: portaudio outputLatency %lf\n", pDev->stream_description, ptStreamInfo->outputLatency); + } + if (error == paNoError) + return 0; + return 1; +} + +void quisk_start_sound_portaudio(struct sound_dev ** pCapture, struct sound_dev ** pPlayback) +{ + int index, err, match; + struct sound_dev ** pCapt, ** pPlay; + + Pa_Initialize(); + // Set the portaudio index from the name. Return on error. + pCapt = pCapture; + while (*pCapt) { + if( (*pCapt)->driver == DEV_DRIVER_PORTAUDIO && quisk_pa_name2index (*pCapt, 1)) + return; // Error + pCapt++; + } + pPlay = pPlayback; + while (*pPlay) { + if( (*pPlay)->driver == DEV_DRIVER_PORTAUDIO && quisk_pa_name2index (*pPlay, 0)) + return; + pPlay++; + } + // Open the sound cards. If a capture device equals a playback device, they must be opened jointly. + pCapt = pCapture; + while (*pCapt) { + index = (*pCapt)->portaudio_index; + if((*pCapt)->driver == DEV_DRIVER_PORTAUDIO && index >= 0) { // This is a portaudio device + pPlay = pPlayback; + match = 0; + while (*pPlay) { + if((*pPlay)->driver == DEV_DRIVER_PORTAUDIO && + (*pPlay)->portaudio_index == index) { // same device, open both + err = quisk_open_portaudio (*pCapt, *pPlay); + match = 1; + break; + } + pPlay++; + } + if ( ! match) + err = quisk_open_portaudio (*pCapt, NULL); // no matching device + if (err) + return; // error + } + pCapt++; + } + strMcpy (quisk_sound_state.msg1, (*pCapture)->msg1, QUISK_SC_SIZE); // Primary capture device + // Open remaining portaudio devices + pPlay = pPlayback; + while (*pPlay) { + if ((*pPlay)->driver == DEV_DRIVER_PORTAUDIO + && (*pPlay)->portaudio_index >= 0 + && ! (*pPlay)->handle + ) { + err = quisk_open_portaudio (NULL, *pPlay); + if (err) + return; // error + } + pPlay++; + } + if ( ! quisk_sound_state.msg1[0]) // Primary playback device + strMcpy (quisk_sound_state.msg1, (*pPlayback)->msg1, QUISK_SC_SIZE); + pCapt = pCapture; + while (*pCapt) { + if ((*pCapt)->handle) + Pa_StartStream((PaStream * )(*pCapt)->handle); + pCapt++; + } + pPlay = pPlayback; + while (*pPlay) { + if ((*pPlay)->handle && Pa_IsStreamStopped((PaStream * )(*pPlay)->handle)) + Pa_StartStream((PaStream * )(*pPlay)->handle); + pPlay++; + } +} + +void quisk_close_sound_portaudio(void) +{ + Pa_Terminate(); +} + +static int device_list(PyObject * py, int input) +{ + int retNum = 0; + char buf100[100]; + + PaError err; + + err = Pa_Initialize(); + + if ( err == paNoError ) { + PaDeviceIndex numDev = Pa_GetDeviceCount(); + for (PaDeviceIndex dev = 0; dev < numDev; dev++) { + const PaDeviceInfo *info = Pa_GetDeviceInfo(dev); +#if (0) + printf("found audio device: %d, name=%s, #inp %d, #outp %d defsample %f\n", dev, info->name, info->maxInputChannels, + info->maxOutputChannels, info->defaultSampleRate); +#endif + if ((input && info->maxInputChannels > 0) || + (!input && info->maxOutputChannels > 0)) { // check the available channels for the type) + // found one + if (py) { + snprintf(buf100, 100, "%s", info->name); + PyList_Append(py, PyString_FromString(buf100)); + } + } + } + Pa_Terminate(); + } + return retNum; + +} + +PyObject * quisk_portaudio_sound_devices(PyObject * self, PyObject * args) +{ // Return a list of portaudio device names [pycapt, pyplay] + PyObject * pylist, * pycapt, * pyplay; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + // Each pycapt and pyplay is [pydev, pyname] + pylist = PyList_New(0); // list [pycapt, pyplay] + pycapt = PyList_New(0); // list of capture devices + pyplay = PyList_New(0); // list of play devices + PyList_Append(pylist, pycapt); + PyList_Append(pylist, pyplay); + device_list(pycapt, 1); + device_list(pyplay, 0); + return pylist; +} +#else // No portaudio support +#include +#include +#include "quisk.h" + +PyObject * quisk_portaudio_sound_devices(PyObject * self, PyObject * args) +{ // Return a list of portaudio device names [pycapt, pyplay] + return quisk_dummy_sound_devices(self, args); +} + +void quisk_start_sound_portaudio(struct sound_dev ** pCapture, struct sound_dev ** pPlayback) +{ + struct sound_dev * pDev; + const char * msg = "No driver support for this device"; + + while (*pCapture) { + pDev = *pCapture++; + if (pDev->driver == DEV_DRIVER_PORTAUDIO) { + strMcpy(pDev->dev_errmsg, msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", msg); + } + } + while (*pPlayback) { + pDev = *pPlayback++; + if (pDev->driver == DEV_DRIVER_PORTAUDIO) { + strMcpy(pDev->dev_errmsg, msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", msg); + } + } +} + +int quisk_read_portaudio(struct sound_dev * dev, complex double * cSamples) +{ + return 0; +} + +void quisk_play_portaudio(struct sound_dev * playdev, int nSamples, complex double * cSamples, int report_latency, double volume) +{} + +void quisk_close_sound_portaudio(void) +{} +#endif diff --git a/sound_pulseaudio.c b/sound_pulseaudio.c new file mode 100644 index 0000000..66bd672 --- /dev/null +++ b/sound_pulseaudio.c @@ -0,0 +1,1236 @@ +/* + * sound_pulseaudio.c is part of Quisk, and is Copyright the following + * authors: + * + * Philip G. Lee , 2014 + * Jim Ahlstrom, N2ADR, October, 2014 + * Eric Thornton, KM4DSJ, September, 2015 + * + * This code replaces the pulseaudio-simple version by Philip G. Lee. It + * uses the asynchronous pulseaudio API. It was written by Eric Thornton, 2015. + * + * Quisk 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 3 of the License, or + * (at your option) any later version. + + * Quisk 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. If not, see . + */ + + +/* This pulseaudio interface was rewritten to utilize the features in the + * asynchronous API and threaded mainloop. + * Eric Thornton, KM4DSJ 2015 +*/ +/* + * 2017 Nov by N2ADR Remove most exit(1) statements. + * 2017 Nov by N2ADR Add code to enable PulseAudio to continue with mis-spelled device names. + * 2020 Apr by N2ADR Changes in printf's +*/ + +#ifdef QUISK_HAVE_PULSEAUDIO + +#include +#include +#include +#include +#include +#include +#include +#include "quisk.h" +#include + +// From pulsecore/macro.h +#define pa_memzero(x,l) (memset((x), 0, (l))) +#define pa_zero(x) (pa_memzero(&(x), sizeof(x))) + +// Current sound status +extern struct sound_conf quisk_sound_state; + +//pointers for aychronous threaded loop +static pa_threaded_mainloop *pa_ml; +static pa_mainloop_api *pa_mlapi; +static pa_context *pa_ctx; //local context +static pa_context *pa_IQ_ctx; //remote context for IQ audio +volatile int streams_ready = 0; //This is ++/-- by the mainloop thread +volatile int streams_to_start; + +// remember all open devices for easy cleanup on exit +static pa_stream *OpenPulseDevices[PA_LIST_SIZE * 2] = {NULL}; + +static int have_QuiskDigitalInput; // Do we need to create this null sink? +static int have_QuiskDigitalOutput; // Do we need to create this null sink? + +static int underrunPlayback; +static int tlength_bytes; + +/* This callback happens any time a stream changes state. Here, it's primary used to + * tell the quisk thread when streams are ready. + */ + +static void stream_state_callback(pa_stream *s, void *userdata) { + struct sound_dev *dev = userdata; + unsigned int S; + assert(s); + assert(dev); + + dev->pulse_stream_state = pa_stream_get_state(s); + switch (dev->pulse_stream_state) { + case PA_STREAM_CREATING: + if (quisk_sound_state.verbose_pulse) + printf("\n**Stream state Creating: %s; %s\n", dev->stream_description, dev->name); + break; + + case PA_STREAM_TERMINATED: + if (quisk_sound_state.verbose_pulse) + printf("\n**Stream state Terminated: %s; %s\n", dev->stream_description, dev->name); + streams_ready--; + break; + + case PA_STREAM_READY: + if (quisk_sound_state.verbose_pulse) + printf("\n**Stream state Ready: %s; %s\n", dev->stream_description, dev->name); + streams_ready++; //increment counter to tell other thread that this stream is ready + streams_to_start++; + if (quisk_sound_state.verbose_pulse) { + const pa_buffer_attr *a; + printf(" Connected to device index %u, %ssuspended: %s.\n", + pa_stream_get_device_index(s), + pa_stream_is_suspended(s) ? "" : "not ", + pa_stream_get_device_name(s)); + + S = dev->num_channels * dev->sample_bytes * dev->sample_rate / 1000; + if (!(a = pa_stream_get_buffer_attr(s))) + printf(" pa_stream_get_buffer_attr() failed: %s", pa_strerror(pa_context_errno(pa_stream_get_context(s)))); + else if (!(a->prebuf)) { + printf(" Buffer metrics msec: rate %d, channels %d, sample bytes %d, maxlength=%u, fragsize=%u\n", + dev->sample_rate, dev->num_channels, dev->sample_bytes, a->maxlength/S, a->fragsize/S); + } + else { + printf(" Buffer metrics msec: rate %d, channels %d, sample bytes %d, maxlength=%u, prebuf=%u, tlength=%u minreq=%u\n", + dev->sample_rate, dev->num_channels, dev->sample_bytes, a->maxlength/S, a->prebuf/S, a->tlength/S, a->minreq/S); + } + } + break; + + case PA_STREAM_FAILED: + default: + snprintf(dev->dev_errmsg, QUISK_SC_SIZE, + "%.60s: %.60s", dev->device_name, pa_strerror(pa_context_errno(pa_stream_get_context(s)))); + snprintf(quisk_sound_state.err_msg, QUISK_SC_SIZE, + "Stream error: %.40s - %.40s", dev->name, pa_strerror(pa_context_errno(pa_stream_get_context(s)))); + if (quisk_sound_state.verbose_pulse) + printf("\n**Stream state Failed: %s; %s\n", dev->stream_description, dev->device_name); + printf("%s\n", quisk_sound_state.err_msg); + streams_to_start++; + break; + } +} + + +//Indicates underflow on passed stream (playback) +static void stream_underflow_callback(pa_stream *s, void *userdata) { + struct sound_dev *dev = userdata; + assert(s); + assert(dev); + + if (quisk_sound_state.verbose_pulse) + printf("Stream underrun %s\n", dev->stream_description); + dev->dev_error++; + if (dev->dev_index == t_Playback) + underrunPlayback = 1; +} + +//Indicates overflow on passed stream (record) +static void stream_overflow_callback(pa_stream *s, void *userdata) { + struct sound_dev *dev = userdata; + assert(s); + + if (quisk_sound_state.verbose_pulse) + printf("Stream overrun %s\n", dev->stream_description); + dev->dev_error++; +} + +//Indicates stream has started +static void stream_started_callback(pa_stream *s, void *userdata) { + struct sound_dev *dev = userdata; + assert(s); + assert(dev); + + if (quisk_sound_state.verbose_pulse) + printf("Stream started %s\n", dev->stream_description); +} + +//Called when cork/uncork has succeeded on passed stream. Signals mainloop when complete. +static void stream_corked_callback(pa_stream *s, int success, void *userdata) { + assert(s); + struct sound_dev *dev = userdata; + + if (success) { + if (quisk_sound_state.verbose_pulse) + printf("Stream cork/uncork %s success\n", dev->stream_description); + pa_threaded_mainloop_signal(pa_ml, 0); + } + else { + if (quisk_sound_state.verbose_pulse) + printf("Stream cork/uncork %s Failure!\n", dev->stream_description); + exit(11); + } +} + +// Called when stream flush has completed. +static void stream_flushed_callback(pa_stream *s, int success, void *userdata) { + assert(s); + struct sound_dev *dev = userdata; + + if (success) { + printf("Stream flush %s success\n", dev->stream_description); + pa_threaded_mainloop_signal(pa_ml, 0); + } + else { + printf("Stream flush %s Failure!\n", dev->stream_description); + exit(12); + } + pa_threaded_mainloop_signal(pa_ml, 0); +} + +// This is called by the play function when the timing structure is updated. +static void stream_timing_callback(pa_stream *s, int success, void *userdata) { + struct sound_dev *dev = userdata; + pa_usec_t l; + int negative = 0; + assert(s); + + if (!success || pa_stream_get_latency(s, &l, &negative) < 0) { + printf("pa_stream_get_latency() failed: %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s)))); + return; + } + + dev->dev_latency = (int)l; + if (negative) + dev->dev_latency *= -1; + pa_threaded_mainloop_signal(pa_ml, 0); +} + +// This is used to change the radio sound playback buffer attributes so the sidetone is faster +static void buffer_attr_callback(pa_stream * s, int success, void * userdata) { + struct sound_dev * dev = userdata; + assert(s); + + if (success) { + printf("Change buffer attr %s success\n", dev->stream_description); + pa_threaded_mainloop_signal(pa_ml, 0); + } + else { + printf("Change buffer attr %s Failure!\n", dev->stream_description); + exit(17); + } +} +void quisk_set_play_attr(struct sound_dev * dev, const pa_buffer_attr * attr) { + pa_stream *s = dev->handle; + pa_operation *o; + + pa_threaded_mainloop_lock(pa_ml); + + if (!(o = pa_stream_set_buffer_attr (s, attr, buffer_attr_callback, dev))) { + printf("quisk_play_attr(): %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s)))); + exit(14); + } + else { + while(pa_operation_get_state(o) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(pa_ml); + pa_operation_unref(o); + } + pa_threaded_mainloop_unlock(pa_ml); +} + + +/* This callback allows us to read in the server side stream information so that we + * can match stream formats and sizes to what is configured in pulseaudio. +*/ + +static void server_info_cb(pa_context *c, const pa_server_info *info, void *userdata) { + int bytes_per_frame; + struct sound_dev **pDevices = userdata; + pa_buffer_attr rec_ba; + pa_buffer_attr play_ba; + pa_sample_spec ss; + pa_sample_spec default_ss; + pa_stream_flags_t pb_flags; + pa_stream_flags_t rec_flags = PA_STREAM_ADJUST_LATENCY; + default_ss = info->sample_spec; + + printf("Connected to %s \n", info->host_name); + + while(*pDevices) { + struct sound_dev *dev = *pDevices++; + const char *dev_name; + pa_stream *s; + + pa_zero(rec_ba); + pa_zero(play_ba); + + dev_name = dev->device_name; + if (strcmp(dev_name, "default") == 0) // the device name is "default" for the default device + dev_name = NULL; + + if (quisk_sound_state.verbose_pulse) + printf("Opening Device %s", dev_name); + + //Construct sample specification. Use S16LE if availiable. Default to Float32 for others. + //If the source/sink is not Float32, Pulseaudio will convert it (uses CPU) + //dev->sample_bytes = (int)pa_frame_size(&ss) / ss.channels; + if (default_ss.format == PA_SAMPLE_S16LE) { + dev->sample_bytes = 2; + ss.format = default_ss.format; + dev->sound_format = Int16; + if (quisk_sound_state.verbose_pulse) + printf(" with sound format Int16\n"); + } + else { + dev->sample_bytes = 4; + ss.format = PA_SAMPLE_FLOAT32LE; + dev->sound_format = Float32; + if (quisk_sound_state.verbose_pulse) + printf(" with sound format Float32\n"); + } + + ss.rate = dev->sample_rate; + ss.channels = dev->num_channels; + bytes_per_frame = dev->sample_bytes * dev->num_channels; + + rec_ba.maxlength = (uint32_t) -1; + rec_ba.fragsize = (uint32_t) SAMP_BUFFER_SIZE / 16; //higher numbers eat cpu on reading monitor streams. + play_ba.maxlength = (uint32_t) -1; + if (dev->dev_index == t_Playback) { // radio sound playback uses cork/uncork instead of prebuf + play_ba.prebuf = 0; + tlength_bytes = dev->sample_rate / 1000 * quisk_sound_state.data_poll_usec * bytes_per_frame * 2 / 1000; + play_ba.tlength = tlength_bytes; + } + else { + play_ba.prebuf = bytes_per_frame * dev->latency_frames; + play_ba.tlength = play_ba.prebuf; + } + + + if (dev->latency_frames == 0) + play_ba.minreq = (uint32_t) 0; //verify this is sane + else + play_ba.minreq = (uint32_t) -1; + if (dev->stream_dir_record) { + + if (!(s = pa_stream_new(c, dev->stream_description, &ss, NULL))) { + printf("pa_stream_new() failed: %s", pa_strerror(pa_context_errno(c))); + continue; + } + if (pa_stream_connect_record(s, dev_name, &rec_ba, rec_flags) < 0) { + printf("pa_stream_connect_record() failed: %s", pa_strerror(pa_context_errno(c))); + continue; + } + pa_stream_set_overflow_callback(s, stream_overflow_callback, dev); + } + + else { + pa_cvolume cv; + pa_volume_t volume = PA_VOLUME_NORM; + + if (!(s = pa_stream_new(c, dev->stream_description, &ss, NULL))) { + printf("pa_stream_new() failed: %s", pa_strerror(pa_context_errno(c))); + continue; + } + if (dev->dev_index == t_Playback) { + pb_flags = PA_STREAM_START_CORKED; + dev->cork_status = 1; + } + else { + pb_flags = PA_STREAM_NOFLAGS; + } + if (pa_stream_connect_playback(s, dev_name, &play_ba, pb_flags, pa_cvolume_set(&cv, ss.channels, volume), NULL) < 0) { + printf("pa_stream_connect_playback() failed: %s", pa_strerror(pa_context_errno(c))); + continue; + } + pa_stream_set_underflow_callback(s, stream_underflow_callback, dev); + } + + + pa_stream_set_state_callback(s, stream_state_callback, dev); + pa_stream_set_started_callback(s, stream_started_callback, dev); + + dev->handle = (void*)s; //save memory address for stream in handle + + int i; + for(i=0;i < PA_LIST_SIZE;i++) { //save address for stream for easy exit + if (!(OpenPulseDevices[i])) { + OpenPulseDevices[i] = dev->handle; + break; + } + } + } +} + + +//Context state callback. Basically here to pass initialization to server_info_cb +static void state_cb(pa_context *c, void *userdata) { + pa_context_state_t state; + state = pa_context_get_state(c); + switch (state) { + // There are just here for reference + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + default: + break; + case PA_CONTEXT_FAILED: + case PA_CONTEXT_TERMINATED: + printf("Context Terminated\n"); + break; + case PA_CONTEXT_READY: { + pa_operation *o; + if (!(o = pa_context_get_server_info(c, server_info_cb, userdata))) + printf("pa_context_get_server_info() failed: %s", pa_strerror(pa_context_errno(c))); + else + pa_operation_unref(o); + } + } +} + + +#if 0 +/* Stream draining complete */ +static void stream_drain_complete(pa_stream*s, int success, void *userdata) { + struct sound_dev *dev = userdata; + + if (!success) { + printf("Failed to drain stream: %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s)))); + quit(1); + } + + if (quisk_sound_state.verbose_pulse) + printf("Playback stream %s drained.\n", dev->name); +} + + +/*drain stream function*/ +void quisk_drain_cork_stream(struct sound_dev *dev) { + pa_stream *s = dev->handle; + pa_operation *o; + + if (!(o = pa_stream_drain(s, stream_drain_complete, NULL))) { + printf("pa_stream_drain(): %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s)))); + exit(1); + } + + pa_operation_unref(o); +} + +#endif + +//Cork function. Holds mainloop lock until operation is completed. +void quisk_cork_pulseaudio(struct sound_dev *dev, int b) { + pa_stream *s = dev->handle; + pa_operation *o; + + pa_threaded_mainloop_lock(pa_ml); + + if (!(o = pa_stream_cork(s, b, stream_corked_callback, dev))) { + printf("pa_stream_cork(): %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s)))); + exit(13); + } + else { + while(pa_operation_get_state(o) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(pa_ml); + + pa_operation_unref(o); + } + pa_threaded_mainloop_unlock(pa_ml); + + if (b) + dev->cork_status = 1; + else + dev->cork_status = 0; +} + +//Flush function. Holds mainloop lock until operation is completed. +void quisk_flush_pulseaudio(struct sound_dev *dev) { + pa_stream *s = dev->handle; + pa_operation *o; + + pa_threaded_mainloop_lock(pa_ml); + + if (!(o = pa_stream_flush(s, stream_flushed_callback, dev))) { + printf("pa_stream_flush(): %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s)))); + exit(14); + } + else { + while(pa_operation_get_state(o) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(pa_ml); + pa_operation_unref(o); + } + pa_threaded_mainloop_unlock(pa_ml); +} + +static void WaitForPoll(void) { // Implement a blocking read + static double time0 = 0; // start time in seconds + double timer; // time remaining from last poll usec + + timer = quisk_sound_state.data_poll_usec - (QuiskTimeSec() - time0) * 1e6; + + if (timer > 1000.0) // see if enough time has elapsed + QuiskSleepMicrosec((int)timer); // wait for the remainder of the poll interval + + time0 = QuiskTimeSec(); // reset starting time value +} + + +/* Read samples from the PulseAudio device. + * Samples are converted to complex form based upon format, counted + * and returned via cSamples pointer. + * Returns the number of samples placed into cSamples + */ +int quisk_read_pulseaudio(struct sound_dev *dev, complex double *cSamples) +{ // cSamples can be NULL to discard samples + int i, nSamples; + int read_frames; // A frame is a sample from each channel + const void * fbuffer; + pa_stream *s = dev->handle; + size_t read_bytes; + + if (!dev || dev->pulse_stream_state != PA_STREAM_READY) + return 0; + + if (dev->cork_status) { + if (dev->read_frames != 0) { + WaitForPoll(); + } + return 0; + } + + // Note: Access to PulseAudio data from our sound thread requires locking the threaded mainloop. + if (dev->read_frames == 0) { // non-blocking: read available frames + pa_threaded_mainloop_lock(pa_ml); + read_frames = pa_stream_readable_size(s) / dev->num_channels / dev->sample_bytes; + + if (read_frames == 0) { + pa_threaded_mainloop_unlock(pa_ml); + return 0; + } + dev->dev_latency = read_frames * dev->num_channels * 1000 / (dev->sample_rate / 1000); + + } + + else { // Blocking read for dev->read_frames total frames + WaitForPoll(); + pa_threaded_mainloop_lock(pa_ml); + read_frames = pa_stream_readable_size(s) / dev->num_channels / dev->sample_bytes; + + if (read_frames == 0) { + pa_threaded_mainloop_unlock(pa_ml); + return 0; + } + dev->dev_latency = read_frames * dev->num_channels * 1000 / (dev->sample_rate / 1000); + } + + + nSamples = 0; + + while (nSamples < read_frames) { // read samples in chunks until we have enough samples + if (pa_stream_peek (s, &fbuffer, &read_bytes) < 0) { + printf("Failure of pa_stream_peek in quisk_read_pulseaudio\n"); + pa_threaded_mainloop_unlock(pa_ml); + return nSamples; + } + + if (fbuffer == NULL && read_bytes == 0) { // buffer is empty + break; + } + + if (fbuffer == NULL) { // there is a hole in the buffer + pa_stream_drop(s); + continue; + } + + if (nSamples * dev->num_channels * dev->sample_bytes + read_bytes >= SAMP_BUFFER_SIZE * 8 / 10) { + if (quisk_sound_state.verbose_pulse) + printf("buffer full on %s\n", dev->name); + pa_stream_drop(s); // limit read request to buffer size + break; + } + + // Convert sampled data to complex data. dev->num_channels must be 2. + if (dev->sample_bytes == 4) { //float32 + float fi, fq; + + for( i = 0; i < (int)read_bytes; i += (dev->num_channels * 4)) { + memcpy(&fi, fbuffer + i + (dev->channel_I * 4), 4); + memcpy(&fq, fbuffer + i + (dev->channel_Q * 4), 4); + if (fi >= 1.0 || fi <= -1.0) + dev->overrange++; + if (fq >= 1.0 || fq <= -1.0) + dev->overrange++; + if (cSamples) + cSamples[nSamples] = (fi + I * fq) * CLIP32; + nSamples++; + } + } + + else if (dev->sample_bytes == 2) { //16bit integer little-endian + int16_t si, sq; + for( i = 0; i < (int)read_bytes; i += (dev->num_channels * 2)) { + memcpy(&si, fbuffer + i + (dev->channel_I * 2), 2); + memcpy(&sq, fbuffer + i + (dev->channel_Q * 2), 2); + if (si >= CLIP16 || si <= -CLIP16) + dev->overrange++; + if (sq >= CLIP16 || sq <= -CLIP16) + dev->overrange++; + int ii = si << 16; + int qq = sq << 16; + if (cSamples) + cSamples[nSamples] = ii + I * qq; + nSamples++; + } + } + + else { + printf("Unknown sample size for %s", dev->name); + } + pa_stream_drop(s); + } + pa_threaded_mainloop_unlock(pa_ml); + return nSamples; +} + +/*! + * \Write outgoing samples directly to pulseaudio server. + * \param playdev Input. Device to which to play the samples. + * \param nSamples Input. Number of samples to play. + * \param cSamples Input. Sample buffer to play from. + * \param report_latency Input. 1 to update \c quisk_sound_state.latencyPlay, 0 otherwise. + * \param volume Input. Ratio in [0,1] by which to scale the played samples. + */ +void quisk_play_pulseaudio(struct sound_dev *dev, int nSamples, complex double *cSamples, + int report_latency, double volume) { + pa_stream *s = dev->handle; + static const pa_timing_info * timing = NULL; + int i=0, n=0, bufferFrames, bytes_per_frame; + void *fbuffer; + size_t fbuffer_bytes = 0; + size_t writable; + + if( !dev || nSamples <= 0 || dev->pulse_stream_state != PA_STREAM_READY) + return; + + if (dev->cork_status && dev->dev_index != t_Playback) + return; + + if (report_latency) { // Report the latency, if requested. + pa_operation *o; + + pa_threaded_mainloop_lock(pa_ml); + + if (!(o = pa_stream_update_timing_info(s, stream_timing_callback, dev))) { + printf("pa_stream_update_timing(): %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s)))); + } + else { + while (pa_operation_get_state(o) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(pa_ml); + pa_operation_unref(o); + } + if (timing == NULL) + timing = pa_stream_get_timing_info(s); + + pa_threaded_mainloop_unlock(pa_ml); + + } + + bytes_per_frame = dev->sample_bytes * dev->num_channels; + if (dev->dev_index == t_Playback) { + bufferFrames = (timing->write_index - timing->read_index) / bytes_per_frame; + if (quisk_sound_state.verbose_pulse > 2) + printf("read %ld, write %ld, frames %d\n", timing->read_index, timing->write_index, bufferFrames); + if (dev->cork_status) { + if (bufferFrames >= dev->latency_frames) + quisk_cork_pulseaudio(dev, 0); + } + else { + if (underrunPlayback) { + underrunPlayback = 0; + quisk_cork_pulseaudio(dev, 1); + } + } + } + + fbuffer = pa_xmalloc(nSamples * bytes_per_frame); + + // Convert from complex data to framebuffer + + if (dev->sample_bytes == 4) { + float fi=0.f, fq=0.f; + for(i = 0, n = 0; n < nSamples; i += (dev->num_channels * 4), ++n) { + fi = (volume * creal(cSamples[n])) / CLIP32; + fq = (volume * cimag(cSamples[n])) / CLIP32; + memcpy(fbuffer + i + (dev->channel_I * 4), &fi, 4); + memcpy(fbuffer + i + (dev->channel_Q * 4), &fq, 4); + } + } + + else if (dev->sample_bytes == 2) { + int ii, qq; + for(i = 0, n = 0; n < nSamples; i += (dev->num_channels * 2), ++n) { + ii = (int)(volume * creal(cSamples[n]) / 65536); + qq = (int)(volume * cimag(cSamples[n]) / 65536); + memcpy(fbuffer + i + (dev->channel_I * 2), &ii, 2); + memcpy(fbuffer + i + (dev->channel_Q * 2), &qq, 2); + } + } + + else { + printf("Unknown sample size for %s", dev->name); + exit(15); + } + + + + fbuffer_bytes = nSamples * bytes_per_frame; + pa_threaded_mainloop_lock(pa_ml); + if (dev->dev_index == t_Playback) // defeat pa_stream_writable_size() + writable = 1024 * 1000; + else + writable = pa_stream_writable_size(s); + + if (writable > 0) { + if ( writable > 1024*1000 ) //sanity check to prevent pa_xmalloc from crashing on monitor streams + writable = 1024*1000; + if (fbuffer_bytes > writable) { + if (quisk_sound_state.verbose_pulse && dev->dev_index <= t_MicPlayback) // ignore for digital interfaces + printf("Truncating write on %s by %lu bytes\n", dev->stream_description, fbuffer_bytes - writable); + fbuffer_bytes = writable; + } + pa_stream_write(dev->handle, fbuffer, fbuffer_bytes, NULL, 0, PA_SEEK_RELATIVE); + } + else { + if (quisk_sound_state.verbose_pulse && dev->dev_index <= t_MicPlayback) + printf("Can't write to stream %s. Dropping %lu bytes\n", dev->stream_description, fbuffer_bytes); + } + + pa_threaded_mainloop_unlock(pa_ml); + pa_xfree(fbuffer); +} + +void quisk_pulseaudio_sidetone(struct sound_dev * dev) +{ + int i, ch_I, ch_Q, flags, target_frames, fbuffer_bytes, bytes_per_sample, bytes_per_frame, bufferFrames, writeFrames; + static play_state_t old_play_state = RECEIVE; + static const pa_timing_info * timing = NULL; + pa_operation * o; + pa_stream * s = dev->handle; + void * fbuffer, * buffer; + void * ptSample; + + if( !dev || dev->pulse_stream_state != PA_STREAM_READY) + return; + + bytes_per_sample = dev->sample_bytes; + bytes_per_frame = dev->sample_bytes * dev->num_channels; + pa_threaded_mainloop_lock(pa_ml); + if (!(o = pa_stream_update_timing_info(s, stream_timing_callback, dev))) { + printf("pa_stream_update_timing(): %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s)))); + } + else { + while (pa_operation_get_state(o) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(pa_ml); + pa_operation_unref(o); + } + if (timing == NULL) + timing = pa_stream_get_timing_info(s); + pa_threaded_mainloop_unlock(pa_ml); + + if (timing->read_index_corrupt || timing->write_index_corrupt) { + printf ("Index corrupt in quisk_pulseaudio_sidetone()\n"); + return; + } + + bufferFrames = (timing->write_index - timing->read_index) / bytes_per_frame; + if (quisk_sound_state.verbose_pulse > 2) + printf("read %ld, write %ld, frames %d\n", timing->read_index, timing->write_index, bufferFrames); + + if (old_play_state <= RECEIVE && quisk_play_state > RECEIVE) { // change from Rx to Tx + if (quisk_sound_state.verbose_pulse > 2) + printf("\nChange to Tx\n"); + old_play_state = quisk_play_state; + } + else if (old_play_state > RECEIVE && quisk_play_state <= RECEIVE) { // change from Tx to Rx + if (quisk_sound_state.verbose_pulse > 2) + printf("\nChange to Rx\n"); + dev->old_key = 0; + old_play_state = quisk_play_state; + writeFrames = dev->latency_frames; + fbuffer_bytes = writeFrames * bytes_per_frame; + fbuffer = pa_xmalloc(fbuffer_bytes); + memset(fbuffer, 0, fbuffer_bytes); + pa_threaded_mainloop_lock(pa_ml); + pa_stream_write(s, fbuffer, fbuffer_bytes, NULL, 0, PA_SEEK_RELATIVE_ON_READ); + pa_threaded_mainloop_unlock(pa_ml); + pa_xfree(fbuffer); + return; + } + + target_frames = tlength_bytes / bytes_per_frame * 4; + if (QUISK_CWKEY_DOWN != dev->old_key) { // key changed, re-write buffer + dev->old_key = QUISK_CWKEY_DOWN; + writeFrames = target_frames; + if (quisk_sound_state.verbose_pulse > 2) + printf ("\nChange key %d, writeFrames %d\n", QUISK_CWKEY_DOWN, writeFrames); + quisk_make_sidetone(dev, bufferFrames); // rewind sidetone samples + flags = PA_SEEK_RELATIVE_ON_READ; + } + else { // no change to key; keep buffer full + writeFrames = target_frames - bufferFrames; + if (quisk_sound_state.verbose_pulse > 2) + printf("writeFrames %d\n", writeFrames); + flags = PA_SEEK_RELATIVE; + } + + if (writeFrames <= 0) + return; + fbuffer_bytes = writeFrames * bytes_per_frame; + fbuffer = pa_xmalloc(fbuffer_bytes); + + buffer = fbuffer; + ch_I = dev->channel_I; + ch_Q = dev->channel_Q; + for (i = 0; i < writeFrames; i++) { + ptSample = quisk_make_sidetone(dev, 0); + memcpy(buffer + ch_I * bytes_per_sample, ptSample, bytes_per_sample); + memcpy(buffer + ch_Q * bytes_per_sample, ptSample, bytes_per_sample); + buffer += bytes_per_frame; + } + + pa_threaded_mainloop_lock(pa_ml); + pa_stream_write(s, fbuffer, fbuffer_bytes, NULL, 0, flags); + pa_threaded_mainloop_unlock(pa_ml); + pa_xfree(fbuffer); +} + +//This is a function to sort the device list into local and remote lists. +static void sort_devices(struct sound_dev **plist, struct sound_dev **pLocal, struct sound_dev **pRemote) { + + while(*plist) { + struct sound_dev *dev = *plist++; + + dev->pulse_stream_state = PA_STREAM_UNCONNECTED; + // Not a PulseAudio device + if( dev->driver != DEV_DRIVER_PULSEAUDIO ) + continue; + + // Device without name: sad. + if( !dev->name[0] ) + continue; + + // This is a remote device + if(dev->server[0]) { + int i; + for(i=0;i < PA_LIST_SIZE;i++) { + if (!(*(pRemote+i))) { + *(pRemote+i) = dev; + break; + } + } + } + + // This is a local device + else { + int i; + for(i=0;i < PA_LIST_SIZE; i++) { + if (!(*(pLocal+i))) { + *(pLocal+i) = dev; + break; + } + } + } + } +} + + +/*! + * \brief Search for and open PulseAudio devices. + * + * \param pCapture Input/Output. Array of capture devices to search through. + * If a device has its \c sound_dev.driver field set to + * \c DEV_DRIVER_PULSEAUDIO, it will be opened for recording. + * \param pPlayback Input/Output. Array of playback devices to search through. + * If a device has its \c sound_dev.driver field set to + * \c DEV_DRIVER_PULSEAUDIO, it will be opened for recording. + */ + +//sound_dev ** pointer(address) for list of addresses +//sound_dev * fpointer(address) for list of addresses + +void quisk_start_sound_pulseaudio(struct sound_dev **pCapture, struct sound_dev **pPlayback) { + int num_pa_devices = 0; + int i; + //sorted lists of local and remote devices + struct sound_dev *LocalPulseDevices[PA_LIST_SIZE] = {NULL}; + struct sound_dev *RemotePulseDevices[PA_LIST_SIZE] = {NULL}; + + sort_devices(pCapture, LocalPulseDevices, RemotePulseDevices); + sort_devices(pPlayback, LocalPulseDevices, RemotePulseDevices); + pa_IQ_ctx = NULL; + pa_ctx = NULL; + pa_ml = NULL; + pa_mlapi = NULL; + streams_to_start = 0; + //quisk_sound_state.verbose_pulse = 1; + + if (!RemotePulseDevices[0] && !LocalPulseDevices[0]) { + if (quisk_sound_state.verbose_pulse) + printf("No pulseaudio devices to open!\n"); + return; //nothing to open. No need to start the mainloop. + } + + // Create a mainloop API + pa_ml = pa_threaded_mainloop_new(); + pa_mlapi = pa_threaded_mainloop_get_api(pa_ml); + + assert(pa_signal_init(pa_mlapi) == 0); + + if (pa_threaded_mainloop_start(pa_ml) < 0) { + printf("pa_mainloop_run() failed."); + return; + } + else + printf("Pulseaudio threaded mainloop started\n"); + + pa_threaded_mainloop_lock(pa_ml); + + if (RemotePulseDevices[0]) { //we've got at least 1 remote device + pa_IQ_ctx = pa_context_new(pa_mlapi, "Quisk-remote"); + if (pa_context_connect(pa_IQ_ctx, quisk_sound_state.IQ_server, 0, NULL) < 0) + printf("Failed to connect to remote Pulseaudio server\n"); + pa_context_set_state_callback(pa_IQ_ctx, state_cb, RemotePulseDevices); //send a list of remote devices to open + } + + if (LocalPulseDevices[0]) { //we've got at least 1 local device + pa_ctx = pa_context_new(pa_mlapi, "Quisk-local"); + if (pa_context_connect(pa_ctx, NULL, 0, NULL) < 0) + printf("Failed to connect to local Pulseaudio server\n"); + pa_context_set_state_callback(pa_ctx, state_cb, LocalPulseDevices); + } + + + pa_threaded_mainloop_unlock(pa_ml); + + for(i=0; LocalPulseDevices[i]; i++) { + num_pa_devices++; + } + + for(i=0; RemotePulseDevices[i]; i++) { + num_pa_devices++; + } + + if (quisk_sound_state.verbose_pulse) + printf("Waiting for %d streams to start\n", num_pa_devices); + + while (streams_to_start < num_pa_devices); // wait for all the devices to open + + if (quisk_sound_state.verbose_pulse) + printf("All streams started\n"); + + +} + + +// Close all streams/contexts/loops and return +void quisk_close_sound_pulseaudio() { + int i = 0; + + if (quisk_sound_state.verbose_pulse) + printf("Closing Pulseaudio interfaces \n "); + + while (OpenPulseDevices[i]) { + pa_stream_disconnect(OpenPulseDevices[i]); + pa_stream_unref(OpenPulseDevices[i]); + OpenPulseDevices[i] = '\0'; + i++; + } + + + if (quisk_sound_state.verbose_pulse) + printf("Waiting for %d streams to disconnect\n", streams_ready); + + while(streams_ready > 0); + + if (pa_IQ_ctx) { + pa_context_disconnect(pa_IQ_ctx); + pa_context_unref(pa_IQ_ctx); + pa_IQ_ctx = NULL; + } + + if (pa_ctx) { + pa_context_disconnect(pa_ctx); + pa_context_unref(pa_ctx); + pa_ctx = NULL; + } + + if (pa_ml) { + pa_threaded_mainloop_stop(pa_ml); + pa_threaded_mainloop_free(pa_ml); + pa_ml = NULL; + } +} + + + +// Additional bugs added by N2ADR below this point. +// Code for quisk_pulseaudio_sound_devices is based on code by Igor Brezac and Eric Connell, and Jan Newmarch. +// This should only be called when Quisk first starts, but names changed to protect the other mainloop. + +// This callback gets called when our context changes state. We really only +// care about when it's ready or if it has failed. +static void pa_names_state_cb(pa_context *c, void *userdata) { + pa_context_state_t ctx_state; + int *main_state = userdata; + + ctx_state = pa_context_get_state(c); + switch (ctx_state) { + // There are just here for reference + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + default: + break; + case PA_CONTEXT_FAILED: + case PA_CONTEXT_TERMINATED: + *main_state = 9; + break; + case PA_CONTEXT_READY: + *main_state = 1; + break; + } +} + +static void source_sink(const char * name, const char * descr, pa_proplist * props, PyObject * pylist) { + const char * value; + char buf300[300]; + PyObject * pytup; + + pytup = PyTuple_New(3); + PyList_Append(pylist, pytup); + PyTuple_SET_ITEM(pytup, 0, PyString_FromString(name)); + PyTuple_SET_ITEM(pytup, 1, PyString_FromString(descr)); + value = pa_proplist_gets(props, "device.api"); + + if (value && ! strcmp(value, "alsa")) { + snprintf(buf300, 300, "%s %s (hw:%s,%s)", pa_proplist_gets(props, "alsa.card_name"), pa_proplist_gets(props, "alsa.name"), + pa_proplist_gets(props, "alsa.card"), pa_proplist_gets(props, "alsa.device")); + + PyTuple_SET_ITEM(pytup, 2, PyString_FromString(buf300)); + } + else { + PyTuple_SET_ITEM(pytup, 2, PyString_FromString("")); + } +} + + + + +// pa_mainloop will call this function when it's ready to tell us about a sink. +static void pa_sinklist_cb(pa_context *c, const pa_sink_info *l, int eol, void *userdata) { + if (eol > 0) // If eol is set to a positive number, you're at the end of the list + return; + source_sink(l->name, l->description, l->proplist, (PyObject *)userdata); + if ( ! strcmp(l->name, "QuiskDigitalInput")) + have_QuiskDigitalInput = 1; + if ( ! strcmp(l->name, "QuiskDigitalOutput")) + have_QuiskDigitalOutput = 1; +} + +static void pa_sourcelist_cb(pa_context *c, const pa_source_info *l, int eol, void *userdata) { + if (eol > 0) + return; + source_sink(l->name, l->description, l->proplist, (PyObject *)userdata); +} + +static void index_callback(pa_context *c, uint32_t idx, void *userdata) { + //printf("%u\n", idx); +} + +PyObject * quisk_pulseaudio_sound_devices(PyObject * self, PyObject * args) +{ // Return a list of PulseAudio device names [pycapt, pyplay] + PyObject * pylist, * pycapt, * pyplay; + pa_mainloop *pa_names_ml; + pa_mainloop_api *pa_names_mlapi; + pa_operation *pa_op=NULL; + pa_context *pa_names_ctx; + int state = 0; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + // Each pycapt and pyplay is (dev name, description, alsa name) + pylist = PyList_New(0); // list [pycapt, pyplay] + pycapt = PyList_New(0); // list of capture devices + pyplay = PyList_New(0); // list of play devices + PyList_Append(pylist, pycapt); + PyList_Append(pylist, pyplay); + + //printf("Starting name loop\n"); + + // Create a mainloop API and connection to the default server + pa_names_ml = pa_mainloop_new(); + pa_names_mlapi = pa_mainloop_get_api(pa_names_ml); + pa_names_ctx = pa_context_new(pa_names_mlapi, "DeviceNames"); + + // This function connects to the pulse server + if (pa_context_connect(pa_names_ctx, NULL, 0, NULL) < 0) { + if (quisk_sound_state.verbose_pulse) + printf("No local daemon to connect to for show_pulse_audio_devices option\n"); + return pylist; + } + + // This function defines a callback so the server will tell us it's state. + pa_context_set_state_callback(pa_names_ctx, pa_names_state_cb, &state); + + // Now we'll enter into an infinite loop until we get the data we receive or if there's an error + while (state < 10) { + switch (state) { + case 0: // We can't do anything until PA is ready + pa_mainloop_iterate(pa_names_ml, 1, NULL); + break; + case 1: + // This sends an operation to the server. pa_sinklist_info is + // our callback function and a pointer to our devicelist will + // be passed to the callback. + pa_op = pa_context_get_sink_info_list(pa_names_ctx, pa_sinklist_cb, pyplay); + // Update state for next iteration through the loop + state++; + pa_mainloop_iterate(pa_names_ml, 1, NULL); + break; + case 2: + // Now we wait for our operation to complete. When it's + // complete our pa_output_devicelist is filled out, and we move + // along to the next state + if (pa_operation_get_state(pa_op) == PA_OPERATION_DONE) { + pa_operation_unref(pa_op); + // Now we perform another operation to get the source + // (input device) list just like before. + pa_op = pa_context_get_source_info_list(pa_names_ctx, pa_sourcelist_cb, pycapt); + // Update the state so we know what to do next + state++; + } + pa_mainloop_iterate(pa_names_ml, 1, NULL); + break; + case 3: + if (pa_operation_get_state(pa_op) == PA_OPERATION_DONE) { + pa_operation_unref(pa_op); + state = 4; + } + else + pa_mainloop_iterate(pa_names_ml, 1, NULL); + break; + // The following loads modules for digital input and output if they are not present. + case 4: + if ( ! have_QuiskDigitalInput) { + pa_op = pa_context_load_module(pa_names_ctx, "module-null-sink", + "sink_name=QuiskDigitalInput sink_properties=device.description=QuiskDigitalInput", + index_callback, NULL); + state = 5; + pa_mainloop_iterate(pa_names_ml, 1, NULL); + } + else + state = 6; + break; + case 5: + if (pa_operation_get_state(pa_op) == PA_OPERATION_DONE) { + pa_operation_unref(pa_op); + state = 6; + } + else + pa_mainloop_iterate(pa_names_ml, 1, NULL); + break; + case 6: + if ( ! have_QuiskDigitalOutput) { + pa_op = pa_context_load_module(pa_names_ctx, "module-null-sink", + "sink_name=QuiskDigitalOutput sink_properties=device.description=QuiskDigitalOutput", + index_callback, NULL); + state = 7; + pa_mainloop_iterate(pa_names_ml, 1, NULL); + } + else + state = 9; + break; + case 7: + if (pa_operation_get_state(pa_op) == PA_OPERATION_DONE) { + pa_operation_unref(pa_op); + state = 9; + } + else + pa_mainloop_iterate(pa_names_ml, 1, NULL); + break; + case 9: // Now we're done, clean up and disconnect and return + pa_context_disconnect(pa_names_ctx); + pa_context_unref(pa_names_ctx); + pa_mainloop_free(pa_names_ml); + state = 99; + break; + } + } + //printf("Finished with name loop\n"); + return pylist; +} +#else // No PulseAudio available +#include +#include +#include "quisk.h" + +PyObject * quisk_pulseaudio_sound_devices(PyObject * self, PyObject * args) +{ // Return a list of PulseAudio device names [pycapt, pyplay] + return quisk_dummy_sound_devices(self, args); +} + +void quisk_start_sound_pulseaudio(struct sound_dev **pCapture, struct sound_dev **pPlayback) +{ + struct sound_dev * pDev; + const char * msg = "No driver support for this device"; + + while (*pCapture) { + pDev = *pCapture++; + if (pDev->driver == DEV_DRIVER_PULSEAUDIO) { + strMcpy(pDev->dev_errmsg, msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", msg); + } + } + while (*pPlayback) { + pDev = *pPlayback++; + if (pDev->driver == DEV_DRIVER_PULSEAUDIO) { + strMcpy(pDev->dev_errmsg, msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", msg); + } + } +} + +int quisk_read_pulseaudio(struct sound_dev *dev, complex double *cSamples) +{ + return 0; +} + +void quisk_play_pulseaudio(struct sound_dev *dev, int nSamples, complex double *cSamples, int report_latency, double volume) +{} + +void quisk_close_sound_pulseaudio() +{} + +void quisk_cork_pulseaudio(struct sound_dev *dev, int b) +{} + +void quisk_flush_pulseaudio(struct sound_dev *dev) +{} +#endif diff --git a/sound_wasapi.c b/sound_wasapi.c new file mode 100644 index 0000000..85ca232 --- /dev/null +++ b/sound_wasapi.c @@ -0,0 +1,1379 @@ +/* + * This module provides sound access for QUISK using the Windows WASAPI library. + + * This software is Copyright (C) 2020 by James C. Ahlstrom, and is + * licensed for use under the GNU General Public License (GPL). + * See http://www.opensource.org. + * Note that there is NO WARRANTY AT ALL. USE AT YOUR OWN RISK!! + +*/ +#ifdef QUISK_HAVE_WASAPI + +#define UNICODE +#include +#include +#include +#include "quisk.h" +#include +#include +#define INITGUID +#define CINTERFACE +#define COBJMACROS +#include +#include +#include +#include +#include +#include +#include +#include + +#define EXIT_ON_ERROR(hres) \ + if (FAILED(hres)) { goto Exit; } + +// REFERENCE_TIME time units per second and per millisecond +#define REFTIMES_PER_SEC 10000000 +#define REFTIMES_PER_MILLISEC 10000 +#define REFTIMES_PER_MICROSEC 10 + +#define PKEY_DEVICE_FRIENDLYNAME (&PKEY_Device_FriendlyName) +#define CLSID_MMDEVICEENUMERATOR (&CLSID_MMDeviceEnumerator) +#define IID_IAUDIOCLIENT (&IID_IAudioClient) +#define IID_IAUDIORENDERCLIENT (&IID_IAudioRenderClient) +#define IID_IAUDIOCAPTURECLIENT (&IID_IAudioCaptureClient) +#define IID_IMMDEVICEENUMERATOR (&IID_IMMDeviceEnumerator) + +const GUID KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = { + 0x00000003,0x0000,0x0010, {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71} +} ; + +const GUID KSDATAFORMAT_SUBTYPE_PCM = { + 0x00000001,0x0000,0x0010, {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71} +} ; + +static IMMDeviceEnumerator *pEnumerator = NULL; + +struct dev_data_t { + IMMDevice *pDevice; + IAudioClient *pAudioClient; + IAudioCaptureClient *pCaptureClient; + IAudioRenderClient *pRenderClient; + UINT32 bufferSizeFrames; // size of the Wasapi buffer in frames + AUDCLNT_SHAREMODE sharemode; + HANDLE hEvent; + int playbuf_underrun_reset; // playbuf is used for the play device threadproc + int playbuf_underrun_msg; + int playbuf_overflow_reset; + int playbuf_nFrames; + _Atomic int64_t playbuf_nRead; // Total number of frames read from buffer + _Atomic int64_t playbuf_nWrite; // Total number of frames written to buffer + unsigned char * playbuf_buf; // frames are in native format +} ; + +static LPWSTR to_pwsz(const char *str) { // Convert UTF-8 string to Wide Character string. + static wchar_t wchar_buffer[256]; + if (MultiByteToWideChar(CP_UTF8, 0, str, -1, wchar_buffer, 256) == 0) + return NULL; + return wchar_buffer; +} + +static void quisk_reset_audio_device(struct sound_dev * dev) +{ // Contributed by Ben Cahill, AC2YD, February 2021 + struct dev_data_t * DD = dev->device_data; + HRESULT hr; + + hr = IAudioClient_Stop(DD->pAudioClient); + if (hr != S_OK) { + QuiskPrintf("%s: IAudioClient_Stop() returned hr = 0x%lx!!\n", dev->stream_description, hr); + } + hr = IAudioClient_Reset(DD->pAudioClient); + if (hr != S_OK) { + QuiskPrintf("%s: IAudioClient_Reset() returned hr = 0x%lx\n!!", dev->stream_description, hr); + } + hr = IAudioClient_Start(DD->pAudioClient); + if (hr != S_OK) { + QuiskPrintf("%s: IAudioClient_Start() returned hr = 0x%lx!!", dev->stream_description, hr); + } +} + +static void close_device(struct sound_dev *, const char *); +static void open_wasapi_playback(struct sound_dev *, int); + +static DWORD WINAPI playdevice_threadproc(LPVOID lpParameter) // Called by a special thread. +{ // Read samples from the playdevice threadproc buffer and write them to the sound card. + struct sound_dev * dev = (struct sound_dev *)lpParameter; + struct dev_data_t * DD = dev->device_data; + DWORD retval; + BYTE * pData; + BYTE * ptSamples; + int64_t nRead, nWrite; + int i, two, ch_I, ch_Q, frames_in_buffer, bytes_per_sample, bytes_per_frame, index, frames_to_end; + DWORD taskIndex = 0; + HANDLE hTask; + UINT32 write_frames; + UINT32 NumPaddingFrames; + if (CoInitializeEx(NULL, COINIT_APARTMENTTHREADED) != S_OK) + QuiskPrintf("%s: CoInitializeEx failed\n", dev->stream_description); + hTask = AvSetMmThreadCharacteristics(TEXT("Pro Audio"), &taskIndex); + if (hTask == NULL) + QuiskPrintf("%s: Could not set thread to Pro Audio\n", dev->stream_description); + if (FAILED(IAudioClient_GetService(DD->pAudioClient, IID_IAUDIORENDERCLIENT, (void**)&DD->pRenderClient))) { + close_device(dev, "Could not create playback client"); + return 1; + } + if (DD->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE) { + // Load the buffer with silence before starting the stream. + if (SUCCEEDED(IAudioRenderClient_GetBuffer(DD->pRenderClient, DD->bufferSizeFrames, &pData))) + IAudioRenderClient_ReleaseBuffer(DD->pRenderClient, DD->bufferSizeFrames, AUDCLNT_BUFFERFLAGS_SILENT); + } + dev->handle = DD->pRenderClient; + if (FAILED(IAudioClient_Start(DD->pAudioClient))) { + close_device(dev, "Could not start"); + return 1; + } + while (1) { + // Wait for next buffer event to be signaled. + retval = WaitForSingleObject(DD->hEvent, 1000); + if (retval != WAIT_OBJECT_0) + break; + if (DD == NULL || dev->handle == NULL) + break; + if (DD->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE) { + write_frames = DD->bufferSizeFrames; + } + else { + if (IAudioClient_GetCurrentPadding(DD->pAudioClient, &NumPaddingFrames) != S_OK) { + QuiskPrintf("%s: playdevice: GetCurrentPadding failed\n", dev->stream_description); + dev->dev_error++; + continue; + } + write_frames = DD->bufferSizeFrames - NumPaddingFrames; + if (write_frames == 0) + continue; + } + if (IAudioRenderClient_GetBuffer(DD->pRenderClient, write_frames, &pData) != S_OK) { + QuiskPrintf("%s: playdevice: GetBuffer failed\n", dev->stream_description); + dev->dev_error++; + continue; + } + if (quisk_play_state < RECEIVE) { // Shutdown or Starting + DD->playbuf_underrun_reset = 0; + IAudioRenderClient_ReleaseBuffer(DD->pRenderClient, write_frames, AUDCLNT_BUFFERFLAGS_SILENT); + continue; + } + nRead = atomic_load(&DD->playbuf_nRead); + nWrite = atomic_load(&DD->playbuf_nWrite); + frames_in_buffer = nWrite - nRead; + if (DD->playbuf_underrun_reset) { + if (frames_in_buffer >= DD->playbuf_nFrames / 2) { + DD->playbuf_underrun_reset = 0; + } + else { + IAudioRenderClient_ReleaseBuffer(DD->pRenderClient, write_frames, AUDCLNT_BUFFERFLAGS_SILENT); + continue; + } + } + else if (frames_in_buffer < write_frames) { + DD->playbuf_underrun_reset = 1; + DD->playbuf_underrun_msg = 1; + dev->dev_underrun++; + IAudioRenderClient_ReleaseBuffer(DD->pRenderClient, write_frames, AUDCLNT_BUFFERFLAGS_SILENT); + continue; + } + two = dev->num_channels >= 2; + ch_I = dev->channel_I; + ch_Q = dev->channel_Q; + bytes_per_sample = dev->sample_bytes; + bytes_per_frame = bytes_per_sample * dev->num_channels; + if ((rxMode == CWU || rxMode == CWL) && quiskSpotLevel < 0 && dev->dev_index == t_MicPlayback) { // SoftRock Tx CW from wasapi + for (i = 0; i < write_frames; i++) { + ptSamples = quisk_make_txIQ(dev, 0); + memcpy(pData + ch_I * bytes_per_sample, ptSamples, bytes_per_sample); + if (two) { + ptSamples += bytes_per_sample; + memcpy(pData + ch_Q * bytes_per_sample, ptSamples, bytes_per_sample); + } + pData += bytes_per_frame; + } + } + else if (quisk_play_state > RECEIVE && quisk_active_sidetone == 1 && dev->dev_index == t_Playback) { // Sidetone from wasapi + for (i = 0; i < write_frames; i++) { + ptSamples = quisk_make_sidetone(dev, 0); + memcpy(pData + ch_I * bytes_per_sample, ptSamples, bytes_per_sample); + if (two) + memcpy(pData + ch_Q * bytes_per_sample, ptSamples, bytes_per_sample); + pData += bytes_per_frame; + } + } + else { // copy sound samples from the buffer to the sound device. + index = nRead % DD->playbuf_nFrames; + frames_to_end = DD->playbuf_nFrames - index; + ptSamples = DD->playbuf_buf + index * bytes_per_frame; + if (write_frames <= frames_to_end) { + memcpy(pData, ptSamples, write_frames * bytes_per_frame); + } + else { + memcpy(pData, ptSamples, frames_to_end * bytes_per_frame); + memcpy(pData + frames_to_end * bytes_per_frame, + DD->playbuf_buf, (write_frames - frames_to_end) * bytes_per_frame); + } + } + IAudioRenderClient_ReleaseBuffer(DD->pRenderClient, write_frames, 0); + nRead = nRead + write_frames; // update number of frames read + atomic_store(&DD->playbuf_nRead, nRead); + } + AvRevertMmThreadCharacteristics(hTask); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s: Exit playback thread\n", dev->stream_description); + CoUninitialize(); + return 0; +} + +void quisk_write_wasapi(struct sound_dev * dev, int nSamples, complex double * cSamples, double volume) +{ // Called from Quisk by the sound thread. Write samples to the playdevice threadproc buffer. + // The ring buffer always stores samples as native frames. + struct dev_data_t * DD = dev->device_data; + int64_t nRead, nWrite; + int i, frames_in_buffer, index, bytes_per_frame, num_channels, ch_I, ch_Q; + int8_t * ptInt8; + int16_t * ptInt16; + int32_t * ptInt32; + float * ptFloat; + int32_t samp32; + unsigned char * buffer_end; + //QuiskPrintTime("write play buffer", 0); + //QuiskMeasureRate("write_wasapi", nSamples, 1, 1); + if (quisk_play_state < RECEIVE) + return; + if (DD == NULL || dev->handle == NULL) + return; + nRead = atomic_load(&DD->playbuf_nRead); + nWrite = atomic_load(&DD->playbuf_nWrite); + frames_in_buffer = nWrite - nRead; + dev->cr_average_fill += (double)(frames_in_buffer + nSamples / 2) / DD->playbuf_nFrames; + dev->cr_average_count++; + dev->dev_latency = frames_in_buffer + nSamples; // frames in buffer available to play + if (quisk_sound_state.verbose_sound) { + if (DD->playbuf_underrun_msg) { + DD->playbuf_underrun_msg = 0; + QuiskPrintf("%s: playbuf underflow\n", dev->stream_description); + } + } + if (DD->playbuf_overflow_reset) { + if (frames_in_buffer <= DD->playbuf_nFrames / 2) { + DD->playbuf_overflow_reset = 0; + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s: play_buffer overflow recovery frames_in_buffer %d, nSamples %d\n", dev->stream_description, + frames_in_buffer, nSamples); + } + else { + //QuiskPrintf("frames_in_buffer %d, nSamples %d\n", frames_in_buffer, nSamples); + return; + } + } + if (frames_in_buffer + nSamples >= DD->playbuf_nFrames) { + dev->dev_error++; + nSamples = DD->playbuf_nFrames * 9 / 10 - frames_in_buffer; // almost fill buffer + if (nSamples < 0) { // buffer is nearly full + DD->playbuf_overflow_reset = 1; + quisk_reset_audio_device(dev); // Ben Cahill: Correct fault in the play callback thread + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s: play_buffer overflow and reset, frames_in_buffer %d, nSamples %d\n", dev->stream_description, frames_in_buffer, nSamples); + } + else if (quisk_sound_state.verbose_sound) { + QuiskPrintf("%s: play_buffer overflow, frames_in_buffer %d, nSamples %d\n", dev->stream_description, frames_in_buffer, nSamples); + } + } + if (nSamples <= 0) + return; + ch_I = dev->channel_I; + ch_Q = dev->channel_Q; + bytes_per_frame = dev->sample_bytes * dev->num_channels; + num_channels = dev->num_channels; + index = nWrite % DD->playbuf_nFrames; + buffer_end = DD->playbuf_buf + DD->playbuf_nFrames * bytes_per_frame; + switch (dev->sound_format) { + case Int16: + ptInt16 = (int16_t *)(DD->playbuf_buf + index * bytes_per_frame); + for (i = 0; i < nSamples; i++) { // for each frame + ptInt16[ch_I] = volume * creal(cSamples[i]) / 65536; + ptInt16[ch_Q] = volume * cimag(cSamples[i]) / 65536; + ptInt16 += num_channels; + if ((unsigned char *)ptInt16 >= buffer_end) { + ptInt16 = (int16_t *)DD->playbuf_buf; + } + } + break; + case Int24: // only works for little-endian + ptInt8 = (int8_t *)(DD->playbuf_buf + index * bytes_per_frame); + for (i = 0; i < nSamples; i++) { + samp32 = volume * creal(cSamples[i]); + memcpy(ptInt8 + ch_I * 3, (int8_t *)&samp32 + 1, 3); + samp32 = volume * cimag(cSamples[i]); + memcpy(ptInt8 + ch_Q * 3, (int8_t *)&samp32 + 1, 3); + ptInt8 += bytes_per_frame; + if ((unsigned char *)ptInt8 >= buffer_end) { + ptInt8 = (int8_t *)DD->playbuf_buf; + } + } + break; + case Int32: + ptInt32 = (int32_t *)(DD->playbuf_buf + index * bytes_per_frame); + for (i = 0; i < nSamples; i++) { + ptInt32[ch_I] = volume * creal(cSamples[i]); + ptInt32[ch_Q] = volume * cimag(cSamples[i]); + ptInt32 += num_channels; + if ((unsigned char *)ptInt32 >= buffer_end) { + ptInt32 = (int32_t *)DD->playbuf_buf; + } + } + break; + case Float32: + ptFloat = (float *)(DD->playbuf_buf + index * bytes_per_frame); + for (i = 0; i < nSamples; i++) { + ptFloat[ch_I] = volume * creal(cSamples[i]) / CLIP32; + ptFloat[ch_Q] = volume * cimag(cSamples[i]) / CLIP32; + ptFloat += num_channels; + if ((unsigned char *)ptFloat >= buffer_end) { + ptFloat = (float *)DD->playbuf_buf; + } + } + break; + } + nWrite = nWrite + nSamples; + atomic_store(&DD->playbuf_nWrite, nWrite); +} + +int quisk_read_wasapi(struct sound_dev * dev, complex double * cSamples) +{ // Called from Quisk by the sound thread. Read samples from the sound card capture buffer. + // cSamples can be NULL to discard samples. + struct dev_data_t * DD = dev->device_data; + int i, bytes_per_frame, num_channels, ch_I, ch_Q; + BYTE * pData; + UINT32 numFramesAvailable; + DWORD flags; + double samp_r, samp_i; + int8_t * ptInt8; + int16_t * ptInt16; + int32_t * ptInt32; + float * ptFloat32; + int32_t iReal, iImag; + int nSamples; + HRESULT hr; + + if (DD == NULL || dev->handle == NULL) + return 0; + + nSamples = 0; + ch_I = dev->channel_I; + ch_Q = dev->channel_Q; + bytes_per_frame = dev->sample_bytes * dev->num_channels; + num_channels = dev->num_channels; + while (1) { // Get the available data + hr = IAudioCaptureClient_GetBuffer(DD->pCaptureClient, &pData, &numFramesAvailable, &flags, NULL, NULL); + if (hr == AUDCLNT_S_BUFFER_EMPTY) { + break; // always use non-blocking read + } + if (hr != S_OK) { // Error + dev->dev_error++; + QuiskPrintf("%s: Error processing buffer\n", dev->stream_description); + break; + } + // There is data to read + if (cSamples) { + if (flags & AUDCLNT_BUFFERFLAGS_SILENT) { + for (i = 0; i < numFramesAvailable; i++) + cSamples[nSamples++] = 0; + } + else switch (dev->sound_format) { + case Int16: + ptInt16 = (int16_t *)pData; + for (i = 0; i < numFramesAvailable; i++) { + samp_r = ptInt16[ch_I]; + samp_i = ptInt16[ch_Q]; + cSamples[nSamples++] = (samp_r + I * samp_i) * CLIP16; + ptInt16 += num_channels; + } + break; + case Int24: // only works for little-endian + ptInt8 = (int8_t *)pData; + for (i = 0; i < numFramesAvailable; i++) { + iReal = 0; + memcpy((int8_t *)&iReal + 1, ptInt8 + ch_I * 3, 3); + iImag = 0; + memcpy((int8_t *)&iImag + 1, ptInt8 + ch_Q * 3, 3); + cSamples[nSamples++] = (iReal + I * iImag); + ptInt8 += bytes_per_frame; + } + break; + case Int32: + ptInt32 = (int32_t *)pData; + for (i = 0; i < numFramesAvailable; i++) { + samp_r = ptInt32[ch_I]; + samp_i = ptInt32[ch_Q]; + cSamples[nSamples++] = (samp_r + I * samp_i); + ptInt32 += num_channels; + } + break; + case Float32: + ptFloat32 = (float *)pData; + for (i = 0; i < numFramesAvailable; i++) { + samp_r = ptFloat32[ch_I]; + samp_i = ptFloat32[ch_Q]; + cSamples[nSamples++] = (samp_r + I * samp_i) * CLIP32; + ptFloat32 += num_channels; + } + break; + } + } + if (FAILED(IAudioCaptureClient_ReleaseBuffer(DD->pCaptureClient, numFramesAvailable))) { + dev->dev_error++; + QuiskPrintf("%s: Failure to release buffer\n", dev->stream_description); + break; + } + } + dev->dev_latency = nSamples; + return nSamples; +} + +#if 0 +static void MeasureTimerPrecision(void) +{ + DWORD t1, t2; + int state = 1; + + t1 = timeGetTime(); + while (state) { + switch (state) { + case 1: + t1 = timeGetTime(); + state = 2; + break; + case 2: + t2 = timeGetTime(); + if (t1 != t2) { + t1 = t2; + state = 3; + } + break; + case 3: + t2 = timeGetTime(); + if (t1 != t2) { + QuiskPrintf("MeasureTimerPrecision %d millisec\n", t2 - t1); + t1 = t2; + state = 4; + } + break; + case 4: + t2 = timeGetTime(); + if (t1 != t2) { + QuiskPrintf("MeasureTimerPrecision %d millisec\n", t2 - t1); + t1 = t2; + state = 0; + } + break; + } + } +} +#endif + +void quisk_play_wasapi(struct sound_dev * dev, int nSamples, complex double * cSamples, double volume) +{ // Called from Quisk by the sound thread. Write samples to the sound card buffer. + UINT32 numFramesPadding; + BYTE *pData; + int8_t * ptb; + int16_t * pts; + int32_t * ptl; + float * ptf; + int32_t samp32; + struct dev_data_t * DD = dev->device_data; + double buffer_fill; + int n, frames, bytes_per_frame; + + if (DD == NULL || dev->handle == NULL) + return; + if (FAILED(IAudioClient_GetCurrentPadding(DD->pAudioClient, &numFramesPadding))) { + QuiskPrintf("%s: quisk_play_wasapi failed to get padding\n", dev->stream_description); + return; + } + buffer_fill = (double)(numFramesPadding + nSamples / 2) / DD->bufferSizeFrames; + dev->cr_average_fill += buffer_fill; + dev->cr_average_count++; + dev->dev_latency = numFramesPadding + nSamples; // frames in buffer available to play + switch(dev->started) { + case 0: // Starting state; wait for samples to become regular + if (quisk_play_state < RECEIVE) { + frames = DD->bufferSizeFrames / 2 - (int)numFramesPadding; + if (frames > 0 && SUCCEEDED(IAudioRenderClient_GetBuffer(DD->pRenderClient, frames, &pData))) { + IAudioRenderClient_ReleaseBuffer(DD->pRenderClient, frames, AUDCLNT_BUFFERFLAGS_SILENT); + } + return; + } + dev->started = 1; + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s: Starting\n", dev->stream_description); + // FALL THRU + case 1: // Normal run state + // Check for underrun + if (numFramesPadding <= dev->sample_rate / 200) { // mimumum play time in buffer + quisk_sound_state.underrun_error++; + dev->dev_underrun++; + if (quisk_sound_state.verbose_sound) + QuiskPrintf ("%s: Underrun error\n", dev->stream_description); + for (nSamples = 0; nSamples < DD->bufferSizeFrames / 2; nSamples++) + cSamples[nSamples] = 0; // fill with silence + buffer_fill = 0.5; + } + // Check if play buffer is too full. + if (numFramesPadding + nSamples >= DD->bufferSizeFrames) { + quisk_sound_state.write_error++; + dev->dev_error++; + if (quisk_sound_state.verbose_sound) + QuiskPrintf ("%s: Buffer too full\n", dev->stream_description); + nSamples = 0; + dev->started = 2; + } + break; + case 2: // Buffer is too full; wait for it to drain + if (buffer_fill <= 0.5) { + dev->started = 1; + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s: Resume adding samples\n", dev->stream_description); + } + else { + nSamples = 0; + } + break; + } + if (nSamples <= 0) + return; + if (FAILED(IAudioRenderClient_GetBuffer(DD->pRenderClient, nSamples, &pData))) { + QuiskPrintf("%s: quisk_play_wasapi failed get the buffer\n", dev->stream_description); + return; + } + switch (dev->sound_format) { + case Int16: + pts = (int16_t *)pData; + for (n = 0; n < nSamples; n++) { + pts[dev->channel_I] = (int16_t)(volume * creal(cSamples[n]) / 65536); + pts[dev->channel_Q] = (int16_t)(volume * cimag(cSamples[n]) / 65536); + pts += dev->num_channels; + } + break; + case Int32: + ptl = (int32_t *)pData; + for (n = 0; n < nSamples; n++) { + ptl[dev->channel_I] = (int32_t)(volume * creal(cSamples[n])); + ptl[dev->channel_Q] = (int32_t)(volume * cimag(cSamples[n])); + ptl += dev->num_channels; + } + break; + case Float32: + ptf = (float *)pData; + for (n = 0; n < nSamples; n++) { + ptf[dev->channel_I] = (volume * creal(cSamples[n]) / CLIP32); + ptf[dev->channel_Q] = (volume * cimag(cSamples[n]) / CLIP32); + ptf += dev->num_channels; + } + break; + case Int24: // only works for little-endian + ptb = (int8_t *)pData; + bytes_per_frame = dev->sample_bytes * dev->num_channels; + for (n = 0; n < nSamples; n++) { + samp32 = volume * creal(cSamples[n]); + memcpy(ptb + dev->channel_I * 3, (int8_t *)&samp32 + 1, 3); + samp32 = volume * cimag(cSamples[n]); + memcpy(ptb + dev->channel_Q * 3, (int8_t *)&samp32 + 1, 3); + ptb += bytes_per_frame; + } + break; + } + IAudioRenderClient_ReleaseBuffer(DD->pRenderClient, nSamples, 0); +} + +static void MakeWFext(int extensible, sound_format_t sound_format, WORD nChannels, struct sound_dev * dev, WAVEFORMATEXTENSIBLE * pwfex) +{ // fill in a WAVEFORMATEXTENSIBLE structure + dev->sound_format = sound_format; + switch (sound_format) { + case Int16: + dev->use_float = 0; + dev->sample_bytes = 2; + break; + case Int24: + dev->use_float = 0; + dev->sample_bytes = 3; + break; + case Int32: + dev->use_float = 0; + dev->sample_bytes = 4; + break; + case Float32: + dev->use_float = 1; + dev->sample_bytes = 4; + break; + } + if (extensible) { + pwfex->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + pwfex->Format.cbSize = 22; + pwfex->Samples.wValidBitsPerSample = dev->sample_bytes * 8; + switch (nChannels) { + case 1: + pwfex->dwChannelMask = 0x01; + break; + case 2: + pwfex->dwChannelMask = 0x03; + break; + case 3: + pwfex->dwChannelMask = 0x07; + break; + case 4: + pwfex->dwChannelMask = 0x0F; + break; + case 5: + pwfex->dwChannelMask = 0x1F; + break; + case 6: + pwfex->dwChannelMask = 0x3F; + break; + case 7: + default: + pwfex->dwChannelMask = 0x7F; + break; + } + if (sound_format == Float32) + pwfex->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; + else + pwfex->SubFormat = KSDATAFORMAT_SUBTYPE_PCM; + } + else { + pwfex->Format.cbSize = 0; + if (sound_format == Float32) + pwfex->Format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT; + else + pwfex->Format.wFormatTag = WAVE_FORMAT_PCM; + } + pwfex->Format.nChannels = nChannels; + pwfex->Format.nSamplesPerSec = dev->sample_rate; + pwfex->Format.nAvgBytesPerSec = nChannels * dev->sample_rate * dev->sample_bytes; + pwfex->Format.nBlockAlign = nChannels * dev->sample_bytes; + pwfex->Format.wBitsPerSample = dev->sample_bytes * 8; +} + +static int make_format(struct sound_dev * dev, WAVEFORMATEXTENSIBLE * pWaveFormat, AUDCLNT_SHAREMODE sharemode) +{ + struct dev_data_t * DD = dev->device_data; + WAVEFORMATEX * pEx = (WAVEFORMATEX *)pWaveFormat; + WAVEFORMATEX * pClosestMatch; + WORD nChannels; + sound_format_t sound_format; + HRESULT result; + + nChannels = dev->num_channels; + for (sound_format = 0; sound_format <= Int24; sound_format++) { + MakeWFext(1, sound_format, nChannels, dev, pWaveFormat); + if (sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE) + result = IAudioClient_IsFormatSupported(DD->pAudioClient, sharemode, pEx, NULL); + else { + pClosestMatch = NULL; + result = IAudioClient_IsFormatSupported(DD->pAudioClient, sharemode, pEx, &pClosestMatch); + CoTaskMemFree(pClosestMatch); + } + if (result == S_OK) + return 1; + } + if (dev->dev_index == t_Playback || dev->dev_index == t_MicCapture) { // mono device is OK for radio sound or mic + for (sound_format = 0; sound_format <= Int24; sound_format++) { + MakeWFext(1, sound_format, 1, dev, pWaveFormat); + if (sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE) + result = IAudioClient_IsFormatSupported(DD->pAudioClient, sharemode, pEx, NULL); + else { + pClosestMatch = NULL; + result = IAudioClient_IsFormatSupported(DD->pAudioClient, sharemode, pEx, &pClosestMatch); + CoTaskMemFree(pClosestMatch); + } + if (result == S_OK) { + dev->num_channels = 1; + dev->channel_I = 0; + dev->channel_Q = 0; + return 1; + } + } + } + // Try to open with more channels + for (nChannels = dev->num_channels + 1; nChannels <= 7; nChannels++) { + for (sound_format = 0; sound_format <= Int24; sound_format++) { + MakeWFext(1, sound_format, nChannels, dev, pWaveFormat); + if (sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE) + result = IAudioClient_IsFormatSupported(DD->pAudioClient, sharemode, pEx, NULL); + else { + pClosestMatch = NULL; + result = IAudioClient_IsFormatSupported(DD->pAudioClient, sharemode, pEx, &pClosestMatch); + CoTaskMemFree(pClosestMatch); + } + if (result == S_OK) { + dev->num_channels = nChannels; + return 1; + } + } + } + return 0; +} + +static void close_device(struct sound_dev * dev, const char * msg) +{ + struct dev_data_t * device_data = dev->device_data; + + if (msg) { + snprintf(dev->dev_errmsg, QUISK_SC_SIZE, "%s: %.80s", msg, dev->name); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", dev->dev_errmsg); + } + if (device_data->pAudioClient) + IAudioClient_Stop(device_data->pAudioClient); + if (device_data->pCaptureClient) + IAudioCaptureClient_Release(device_data->pCaptureClient); + if (device_data->pRenderClient) + IAudioRenderClient_Release(device_data->pRenderClient); + if (device_data->pAudioClient) + IAudioClient_Release(device_data->pAudioClient); + if (device_data->pDevice) + IMMDevice_Release(device_data->pDevice); + if (device_data->playbuf_buf) + free(device_data->playbuf_buf); + if (device_data->hEvent) + CloseHandle(device_data->hEvent); + free(device_data); + dev->device_data = NULL; +} + +static void open_wasapi_capture(struct sound_dev * dev) +{ + REFERENCE_TIME hnsRequestedDuration; + REFERENCE_TIME def_period, min_period; + WAVEFORMATEXTENSIBLE wave_format; + LPWSTR pwszID; + struct dev_data_t * DD = dev->device_data; + HRESULT hr; + DWORD err_code; + + dev->dev_errmsg[0] = 0; + if (quisk_sound_state.verbose_sound) + QuiskPrintf("Opening Wasapi capture device %s\n Name %s\n Device name %s\n", dev->stream_description, dev->name, dev->device_name); + if (pEnumerator == NULL) + return; + + pwszID = to_pwsz(dev->device_name); + if (pwszID == NULL) { + err_code = GetLastError(); + snprintf(dev->dev_errmsg, QUISK_SC_SIZE, "Failure 0x%lX to convert device name: %.80s", err_code, dev->device_name); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", dev->dev_errmsg); + close_device(dev, NULL); + return; + } + if (FAILED(IMMDeviceEnumerator_GetDevice(pEnumerator, pwszID, &DD->pDevice))) { + close_device(dev, "Sound device not found"); + return; + } + + if (FAILED(IMMDevice_Activate(DD->pDevice, IID_IAUDIOCLIENT, CLSCTX_ALL, NULL, (void**)&DD->pAudioClient))) { + close_device(dev, "No audio client"); + return; + } + + if (SUCCEEDED(IAudioClient_GetDevicePeriod(DD->pAudioClient, &def_period, &min_period))) { + if (quisk_sound_state.verbose_sound) + QuiskPrintf(" Device default period %d, min period %d microsec\n", + (int)def_period / REFTIMES_PER_MICROSEC, (int)min_period / REFTIMES_PER_MICROSEC); + } + + if (make_format(dev, &wave_format, AUDCLNT_SHAREMODE_EXCLUSIVE) == 0) { // failure + if (make_format(dev, &wave_format, AUDCLNT_SHAREMODE_SHARED) == 0) { // failure + close_device(dev, "Device can not support the sample rate or number of channels."); + return; + } + DD->sharemode = AUDCLNT_SHAREMODE_SHARED; + if (quisk_sound_state.verbose_sound) + QuiskPrintf(" Exclusive mode refused. Open device in Shared mode\n"); + } + else { + DD->sharemode = AUDCLNT_SHAREMODE_EXCLUSIVE; + if (quisk_sound_state.verbose_sound) + QuiskPrintf(" Open device in Exclusive mode\n"); + } + + if (quisk_sound_state.verbose_sound) { + QuiskPrintf(" Sample rate %d\n Channel_I %d\n Channel_Q %d\n", + dev->sample_rate, dev->channel_I, dev->channel_Q); + QuiskPrintf(" Sound device format %s\n", sound_format_names[dev->sound_format]); + QuiskPrintf(" Number of channels %d, bytes per sample %d\n", dev->num_channels, dev->sample_bytes); + } + hnsRequestedDuration = REFTIMES_PER_MILLISEC * quisk_sound_state.latency_millisecs; + hr = IAudioClient_Initialize(DD->pAudioClient, DD->sharemode, 0, hnsRequestedDuration, 0, (WAVEFORMATEX *)&wave_format, NULL); + if (FAILED(hr)) { + for ( ;hnsRequestedDuration > REFTIMES_PER_MILLISEC * 20; hnsRequestedDuration -= (REFTIMES_PER_MILLISEC * 20)) { + hr = IAudioClient_Initialize(DD->pAudioClient, DD->sharemode, 0, hnsRequestedDuration, 0, (WAVEFORMATEX *)&wave_format, NULL); + if (hr == S_OK) + break; + } + } + if (FAILED(hr)) { + close_device(dev, "Could not initialize"); + return; + } + + // Get the size of the allocated buffer. + if (FAILED(IAudioClient_GetBufferSize(DD->pAudioClient, &DD->bufferSizeFrames))) { + close_device(dev, "Could not get buffer size"); + return; + } + dev->play_buf_size = DD->bufferSizeFrames * dev->sample_bytes * dev->num_channels; + + if (FAILED(IAudioClient_GetService(DD->pAudioClient, IID_IAUDIOCAPTURECLIENT, (void**)&DD->pCaptureClient))) { + close_device(dev, "Could not create capture client"); + return; + } + + dev->handle = DD->pCaptureClient; + + if (FAILED(IAudioClient_Start(DD->pAudioClient))) { + close_device(dev, "Could not start"); + return; + } + if (quisk_sound_state.verbose_sound) { + QuiskPrintf(" Capture Wasapi buffer size %d frames\n Started.\n", DD->bufferSizeFrames); + } +} + +static void open_wasapi_playback(struct sound_dev * dev, int use_callback) +{ + REFERENCE_TIME hnsRequestedDuration; + REFERENCE_TIME def_period, min_period; + WAVEFORMATEXTENSIBLE wave_format; + int i; + BYTE *pData; + LPWSTR pwszID; + struct dev_data_t * DD = dev->device_data; + HRESULT hr = S_OK; + HANDLE hThread = NULL; + DWORD ThreadID; + DWORD err_code; + + dev->dev_errmsg[0] = 0; + if (quisk_sound_state.verbose_sound) { + QuiskPrintf("Opening Wasapi playback device %s\n Name %s\n Device name %s\n", dev->stream_description, dev->name, dev->device_name); + } + if (pEnumerator == NULL) + return; + + pwszID = to_pwsz(dev->device_name); + if (pwszID == NULL) { + err_code = GetLastError(); + snprintf(dev->dev_errmsg, QUISK_SC_SIZE, "Failure 0x%lX to convert device name: %.80s", err_code, dev->device_name); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", dev->dev_errmsg); + close_device(dev, NULL); + return; + } + if (FAILED(IMMDeviceEnumerator_GetDevice(pEnumerator, pwszID, &DD->pDevice))) { + close_device(dev, "Sound device not found"); + return; + } + + if (FAILED(IMMDevice_Activate(DD->pDevice, IID_IAUDIOCLIENT, CLSCTX_ALL, NULL, (void**)&DD->pAudioClient))) { + close_device(dev, "No audio client"); + return; + } + + if (SUCCEEDED(IAudioClient_GetDevicePeriod(DD->pAudioClient, &def_period, &min_period))) { + if (quisk_sound_state.verbose_sound) + QuiskPrintf(" Device default period %d, min period %d microsec\n", + (int)def_period / REFTIMES_PER_MICROSEC, (int)min_period / REFTIMES_PER_MICROSEC); + } + + if (make_format(dev, &wave_format, AUDCLNT_SHAREMODE_EXCLUSIVE) == 0) { // failure + if (make_format(dev, &wave_format, AUDCLNT_SHAREMODE_SHARED) == 0) { // failure + close_device(dev, "Device can not support the sample rate or number of channels."); + return; + } + DD->sharemode = AUDCLNT_SHAREMODE_SHARED; + if (quisk_sound_state.verbose_sound) + QuiskPrintf(" Exclusive mode refused. Open device in Shared mode\n"); + } + else { + DD->sharemode = AUDCLNT_SHAREMODE_EXCLUSIVE; + if (quisk_sound_state.verbose_sound) + QuiskPrintf(" Open device in Exclusive mode\n"); + } + if (quisk_sound_state.verbose_sound) { + QuiskPrintf(" Sample rate %d\n Channel_I %d\n Channel_Q %d\n", + dev->sample_rate, dev->channel_I, dev->channel_Q); + QuiskPrintf(" Sound device format %s\n", sound_format_names[dev->sound_format]); + QuiskPrintf(" Number of channels %d, bytes per sample %d\n", dev->num_channels, dev->sample_bytes); + //MeasureTimerPrecision(); + } + if (use_callback) { + if (DD->sharemode == AUDCLNT_SHAREMODE_EXCLUSIVE) { + hnsRequestedDuration = REFTIMES_PER_MICROSEC * quisk_sound_state.data_poll_usec; + if (hnsRequestedDuration < min_period) + hnsRequestedDuration = min_period; + } + else { + hnsRequestedDuration = 0; + } + hr = IAudioClient_Initialize(DD->pAudioClient, DD->sharemode, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + hnsRequestedDuration, hnsRequestedDuration, (WAVEFORMATEX *)&wave_format, NULL); + if (hr == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) { + if (quisk_sound_state.verbose_sound) + QuiskPrintf(" Fix for buffer size not aligned\n"); + // Get the size of the aligned buffer. + if (FAILED(IAudioClient_GetBufferSize(DD->pAudioClient, &DD->bufferSizeFrames))) { + close_device(dev, "Could not get buffer size"); + return; + } + hnsRequestedDuration = (REFERENCE_TIME)((10000.0 * 1000 / wave_format.Format.nSamplesPerSec * DD->bufferSizeFrames) + 0.5); + IAudioClient_Release(DD->pAudioClient); + if (FAILED(IMMDevice_Activate(DD->pDevice, IID_IAUDIOCLIENT, CLSCTX_ALL, NULL, (void**)&DD->pAudioClient))) { + close_device(dev, "No audio client"); + return; + } + hr = IAudioClient_Initialize(DD->pAudioClient, DD->sharemode, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + hnsRequestedDuration, hnsRequestedDuration, (WAVEFORMATEX *)&wave_format, NULL); + } + if (hr != S_OK) { + close_device(dev, "Could not initialize"); + return; + } + // Get the size of the allocated buffer. + if (FAILED(IAudioClient_GetBufferSize(DD->pAudioClient, &DD->bufferSizeFrames))) { + close_device(dev, "Could not get buffer size"); + return; + } + // Create an event handle and register it for buffer-event notifications. + DD->hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + if (DD->hEvent == NULL) { + close_device(dev, "CreateEvent failed"); + return; + } + if (FAILED(IAudioClient_SetEventHandle(DD->pAudioClient, DD->hEvent))) { + close_device(dev, "SetEventHandle failed"); + return; + } + i = dev->latency_frames * 2 / DD->bufferSizeFrames; // playbuf_nFrames is a multiple of bufferSizeFrames + i = ((i + 1) / 2) * 2; // multiple is an even number + if ( i < 4) + i = 4; + DD->playbuf_nFrames = i * DD->bufferSizeFrames; + DD->playbuf_buf = calloc(DD->playbuf_nFrames * dev->sample_bytes * dev->num_channels, 1); + atomic_store(&DD->playbuf_nWrite, i / 2 * DD->bufferSizeFrames); // buffer starts half full + atomic_store(&DD->playbuf_nRead, 0); + DD->playbuf_underrun_reset = 0; + DD->playbuf_underrun_msg = 0; + DD->playbuf_overflow_reset = 0; + hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) playdevice_threadproc, dev, CREATE_SUSPENDED, &ThreadID); + if (hThread == NULL ) { + close_device(dev, "Create thread failed"); + return; + } + dev->play_buf_size = DD->playbuf_nFrames; + if (quisk_sound_state.verbose_sound) { + QuiskPrintf(" Wasapi buffer size %d frames\n", DD->bufferSizeFrames); + QuiskPrintf(" Callback ring buffer size %d frames\n", DD->playbuf_nFrames); + QuiskPrintf(" Starting the callback thread for %s\n", dev->stream_description); + } + ResumeThread(hThread); + } + else { + hnsRequestedDuration = quisk_sound_state.latency_millisecs * REFTIMES_PER_MILLISEC * 2; // reduce this if too large + for ( ;hnsRequestedDuration > REFTIMES_PER_MILLISEC * 20; hnsRequestedDuration -= (REFTIMES_PER_MILLISEC * 20)) { + hr = IAudioClient_Initialize(DD->pAudioClient, DD->sharemode, 0, + hnsRequestedDuration, 0, (WAVEFORMATEX *)&wave_format, NULL); + if (hr == S_OK) + break; + } + if (FAILED(hr)) { + close_device(dev, "Could not initialize"); + return; + } + + // Get the size of the allocated buffer. + if (FAILED(IAudioClient_GetBufferSize(DD->pAudioClient, &DD->bufferSizeFrames))) { + close_device(dev, "Could not get buffer size"); + return; + } + dev->play_buf_size = DD->bufferSizeFrames; + + if (FAILED(IAudioClient_GetService(DD->pAudioClient, IID_IAUDIORENDERCLIENT, (void**)&DD->pRenderClient))) { + close_device(dev, "Could not create playback client"); + return; + } + // Load the buffer with silence before starting the stream. + if (SUCCEEDED(IAudioRenderClient_GetBuffer(DD->pRenderClient, DD->bufferSizeFrames / 2, &pData))) + IAudioRenderClient_ReleaseBuffer(DD->pRenderClient, DD->bufferSizeFrames / 2, AUDCLNT_BUFFERFLAGS_SILENT); + dev->handle = DD->pRenderClient; + if (FAILED(IAudioClient_Start(DD->pAudioClient))) { + close_device(dev, "Could not start"); + return; + } + if (quisk_sound_state.verbose_sound) { + QuiskPrintf(" Playback Wasapi buffer size %d frames\n", DD->bufferSizeFrames); + QuiskPrintf(" Starting playback for %s\n", dev->stream_description); + } + } +} + +void quisk_start_sound_wasapi (struct sound_dev ** pCapture, struct sound_dev ** pPlayback) +{ // Open the sound devices and start them. Called from the sound thread. + struct sound_dev * pDev; + + if (CoInitializeEx(NULL, COINIT_APARTMENTTHREADED) != S_OK) + QuiskPrintf("CoInitializeEx failed\n"); + if (FAILED(CoCreateInstance(CLSID_MMDEVICEENUMERATOR, NULL, CLSCTX_ALL, IID_IMMDEVICEENUMERATOR, (void**)&pEnumerator))) { + QuiskPrintf("CoCreateInstance failed in start_sound_wasapi\n"); + pEnumerator = NULL; + return; + } + while (*pCapture) { + pDev = *pCapture++; + if (pDev->driver == DEV_DRIVER_WASAPI) { + pDev->device_data = calloc(sizeof(struct dev_data_t), 1); + open_wasapi_capture(pDev); + } + } + while (*pPlayback) { + pDev = *pPlayback++; + if (pDev->driver == DEV_DRIVER_WASAPI) { + pDev->device_data = calloc(sizeof(struct dev_data_t), 1); + open_wasapi_playback(pDev, 0); + } + else if (pDev->driver == DEV_DRIVER_WASAPI2) { + pDev->device_data = calloc(sizeof(struct dev_data_t), 1); + open_wasapi_playback(pDev, 1); + } + } +} + +void quisk_close_sound_wasapi(struct sound_dev ** pCapture, struct sound_dev ** pPlayback) +{ + struct sound_dev * pDev; + + while (*pCapture) { + pDev = *pCapture++; + if (pDev->driver == DEV_DRIVER_WASAPI && pDev->device_data) { + pDev->handle = NULL; + Sleep(200); + close_device(pDev, NULL); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("Close %s\n", pDev->stream_description); + } + } + while (*pPlayback) { + pDev = *pPlayback++; + if ((pDev->driver == DEV_DRIVER_WASAPI || pDev->driver == DEV_DRIVER_WASAPI2) && pDev->device_data) { + pDev->handle = NULL; + Sleep(200); + close_device(pDev, NULL); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("Close %s\n", pDev->stream_description); + } + } + if (pEnumerator) + IMMDeviceEnumerator_Release(pEnumerator); + pEnumerator = NULL; + CoUninitialize(); +} + + +PyObject * quisk_wasapi_sound_devices(PyObject * self, PyObject * args) // Called from the GUI thread +{ // Return a list of sound device data [pycapt, pyplay] + PyObject * pylist, * pycapt, * pyplay, * pytup; + HRESULT hr = S_OK; + IMMDeviceEnumerator *pEnum = NULL; + IMMDeviceCollection *pCollection = NULL; + IMMDevice *pEndpoint = NULL; + IPropertyStore *pProps = NULL; + PROPVARIANT varName; + LPWSTR pwszID = NULL; + UINT i, count; + + if (!PyArg_ParseTuple (args, "")) + return NULL; + pylist = PyList_New(0); // list [pycapt, pyplay] + pycapt = PyList_New(0); // list of capture devices (name, id, is_raw) + pyplay = PyList_New(0); // list of play devices + PyList_Append(pylist, pycapt); + PyList_Append(pylist, pyplay); + + hr = CoCreateInstance(CLSID_MMDEVICEENUMERATOR, NULL, CLSCTX_ALL, IID_IMMDEVICEENUMERATOR, (void**)&pEnum); + if (FAILED(hr)) { + QuiskPrintf("CoCreateInstance failed in wasapi_sound_devices\n"); + return pylist; + } + hr = IMMDeviceEnumerator_EnumAudioEndpoints(pEnum, eRender, DEVICE_STATE_ACTIVE, &pCollection); + EXIT_ON_ERROR(hr) + hr = IMMDeviceCollection_GetCount(pCollection, &count); + EXIT_ON_ERROR(hr) + for (i = 0; i < count; i++) { // Get pointer to eRender endpoint number i. + hr = IMMDeviceCollection_Item(pCollection, i, &pEndpoint); + EXIT_ON_ERROR(hr) + hr = IMMDevice_GetId(pEndpoint, &pwszID); + EXIT_ON_ERROR(hr) + hr = IMMDevice_OpenPropertyStore(pEndpoint, STGM_READ, &pProps); + EXIT_ON_ERROR(hr) + PropVariantInit(&varName); + hr = IPropertyStore_GetValue(pProps, &PKEY_Device_FriendlyName, &varName); + EXIT_ON_ERROR(hr) + // data items are (name, id, is_raw) + pytup = PyTuple_New(3); + PyList_Append(pyplay, pytup); + PyTuple_SET_ITEM(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1)); + PyTuple_SET_ITEM(pytup, 1, PyUnicode_FromWideChar(pwszID, -1)); + PyTuple_SET_ITEM(pytup, 2, PyInt_FromLong(1)); + CoTaskMemFree(pwszID); + pwszID = NULL; + PropVariantClear(&varName); + IPropertyStore_Release(pProps); + pProps = NULL; + IMMDevice_Release(pEndpoint); + pEndpoint = NULL; + } + IMMDeviceCollection_Release(pCollection); + pCollection = NULL; + hr = IMMDeviceEnumerator_EnumAudioEndpoints(pEnum, eCapture, DEVICE_STATE_ACTIVE, &pCollection); + EXIT_ON_ERROR(hr) + hr = IMMDeviceCollection_GetCount(pCollection, &count); + EXIT_ON_ERROR(hr) + for (i = 0; i < count; i++) { // Get pointer to eCapture endpoint number i. + hr = IMMDeviceCollection_Item(pCollection, i, &pEndpoint); + EXIT_ON_ERROR(hr) + hr = IMMDevice_GetId(pEndpoint, &pwszID); + EXIT_ON_ERROR(hr) + hr = IMMDevice_OpenPropertyStore(pEndpoint, STGM_READ, &pProps); + EXIT_ON_ERROR(hr) + PropVariantInit(&varName); + hr = IPropertyStore_GetValue(pProps, &PKEY_Device_FriendlyName, &varName); + EXIT_ON_ERROR(hr) + // data items are (name, id, is_raw) + pytup = PyTuple_New(3); + PyList_Append(pycapt, pytup); + PyTuple_SET_ITEM(pytup, 0, PyUnicode_FromWideChar(varName.pwszVal, -1)); + PyTuple_SET_ITEM(pytup, 1, PyUnicode_FromWideChar(pwszID, -1)); + PyTuple_SET_ITEM(pytup, 2, PyInt_FromLong(1)); + CoTaskMemFree(pwszID); + pwszID = NULL; + PropVariantClear(&varName); + IPropertyStore_Release(pProps); + pProps = NULL; + IMMDevice_Release(pEndpoint); + pEndpoint = NULL; + } + IMMDeviceCollection_Release(pCollection); + IMMDeviceEnumerator_Release(pEnum); + return pylist; + +Exit: + if (pwszID) + CoTaskMemFree(pwszID); + if (pCollection) + IMMDeviceCollection_Release(pCollection); + if (pEndpoint) + IMMDevice_Release(pEndpoint); + if (pProps) + IPropertyStore_Release(pProps); + if (pEnum) + IMMDeviceEnumerator_Release(pEnum); + return pylist; +} + +/****** MIDI ******/ + +static void midi_in_devices(PyObject * pylist) +{ // Return a list of MIDI In devices. + UINT nMidiDeviceNum; + MIDIINCAPS caps; + UINT i; + + nMidiDeviceNum = midiInGetNumDevs(); + for (i = 0; i < nMidiDeviceNum; ++i) { + if (midiInGetDevCaps(i, &caps, sizeof(MIDIINCAPS)) == MMSYSERR_NOERROR) + PyList_Append(pylist, PyUnicode_FromWideChar(caps.szPname, -1)); + } +} + +#define MIDI_MAX 6000 + +static char midi_chars[MIDI_MAX]; +static int midi_length; +static HANDLE MIDI_mutex; + +static void CALLBACK MidiInProc(HMIDIIN hMidiIn, UINT wMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2) +{ + int status, note, velocity; + + if (wMsg == MIM_DATA) { + status = dwParam1 & 0xFF; + note = (dwParam1 >> 8) & 0x7F; + velocity = (dwParam1 >> 16) & 0x7F; + while (WaitForSingleObject(MIDI_mutex, 0) == WAIT_TIMEOUT) ; + if (midi_length <= MIDI_MAX - 3) { + midi_chars[midi_length++] = status; + midi_chars[midi_length++] = note; + midi_chars[midi_length++] = velocity; + } + ReleaseMutex(MIDI_mutex); + if (note == dwInstance) { // dwInstance is the CW note + // ignore channel number + if ((status & 0xF0) == 0x90) { // Note ON + if (velocity) + quisk_midi_cwkey = 1; + else + quisk_midi_cwkey = 0; + } + else if ((status & 0xF0) == 0x80) { // Note OFF + quisk_midi_cwkey = 0; + } + } + } +} + +PyObject * quisk_wasapi_control_midi(PyObject * self, PyObject * args, PyObject * keywds) +{ /* Call with keyword arguments ONLY */ +// midiOutShortMsg + static char * kwlist[] = {"client", "device", "close_port", "get_event", "midi_cwkey_note", + "get_in_names", "get_in_devices", NULL} ; + int client, close_port, get_event, get_in_names, get_in_devices; + static int midi_cwkey_note = -1; + char * device; + PyObject * pylist, * pybytes; + static HMIDIIN hMidiDevice = NULL;; + + client = close_port = get_event = get_in_names = get_in_devices = -1; + device = NULL; + if (!PyArg_ParseTupleAndKeywords (args, keywds, "|isiiiii", kwlist, + &client, &device, &close_port, &get_event, &midi_cwkey_note, &get_in_names, &get_in_devices)) + return NULL; + if (close_port == 1) { // shutdown + if (hMidiDevice) { + midiInStop(hMidiDevice); + midiInClose(hMidiDevice); + } + hMidiDevice = NULL; + quisk_midi_cwkey = 0; + } + if (get_in_devices == 1) { // return a list of MIDI devices; just a list of names + pylist = PyList_New(0); + midi_in_devices(pylist); + return pylist; + } + if (get_in_names == 1) { // return a list of MIDI devices; just a list of names + pylist = PyList_New(0); + midi_in_devices(pylist); + return pylist; + } + if (client >= 0) { // open client port + if ( ! MIDI_mutex) + MIDI_mutex = CreateMutex(NULL, FALSE, NULL); + quisk_midi_cwkey = 0; + if (hMidiDevice == NULL) { + if (midiInOpen(&hMidiDevice, client, (DWORD_PTR)(void*)MidiInProc, midi_cwkey_note, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) { + QuiskPrintf("Could not open MIDI device\n"); + hMidiDevice = NULL; + } + else { + midiInStart(hMidiDevice); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("Open MIDI device %d\n", client); + } + } + } + if (get_event == 1) { // poll to get event + if (midi_length != 0) { + while (WaitForSingleObject(MIDI_mutex, 0) == WAIT_TIMEOUT) ; + pybytes = PyByteArray_FromStringAndSize(midi_chars, midi_length); + midi_length = 0; + ReleaseMutex(MIDI_mutex); + return pybytes; + } + } + Py_INCREF (Py_None); + return Py_None; +} +#else // No Wasapi available +#include +#include +#include "quisk.h" + +PyObject * quisk_wasapi_sound_devices(PyObject * self, PyObject * args) // Called from the GUI thread +{ // Return a list of sound device data [pycapt, pyplay] + return quisk_dummy_sound_devices(self, args); +} + +void quisk_start_sound_wasapi (struct sound_dev ** pCapture, struct sound_dev ** pPlayback) +{ + struct sound_dev * pDev; + const char * msg = "No driver support for this device"; + + while (*pCapture) { + pDev = *pCapture++; + if (pDev->driver == DEV_DRIVER_WASAPI) { + strMcpy(pDev->dev_errmsg, msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", msg); + } + } + while (*pPlayback) { + pDev = *pPlayback++; + if (pDev->driver == DEV_DRIVER_WASAPI) { + strMcpy(pDev->dev_errmsg, msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", msg); + } + else if (pDev->driver == DEV_DRIVER_WASAPI2) { + strMcpy(pDev->dev_errmsg, msg, QUISK_SC_SIZE); + if (quisk_sound_state.verbose_sound) + QuiskPrintf("%s\n", msg); + } + } +} + +void quisk_write_wasapi(struct sound_dev * dev, int nSamples, complex double * cSamples, double volume) +{} + +int quisk_read_wasapi(struct sound_dev * dev, complex double * cSamples) +{ + return 0; +} + +void quisk_play_wasapi(struct sound_dev * dev, int nSamples, complex double * cSamples, double volume) +{} + +void quisk_close_sound_wasapi(struct sound_dev ** pCapture, struct sound_dev ** pPlayback) +{} + +// Note: Return values must be kept up to date! +PyObject * quisk_wasapi_control_midi(PyObject * self, PyObject * args, PyObject * keywds) +{ + static char * kwlist[] = {"client", "device", "close_port", "get_event", "midi_cwkey_note", + "get_in_names", "get_in_devices", NULL} ; + int client, close_port, get_event, get_in_names, get_in_devices; + static int midi_cwkey_note = -1; + char * device; + PyObject * pylist; + + client = close_port = get_event = get_in_names = get_in_devices = -1; + device = NULL; + if (!PyArg_ParseTupleAndKeywords (args, keywds, "|isiiiii", kwlist, + &client, &device, &close_port, &get_event, &midi_cwkey_note, &get_in_names, &get_in_devices)) + return NULL; + if (get_in_devices == 1) { // return a list of MIDI devices; just a list of names + pylist = PyList_New(0); + return pylist; + } + if (get_in_names == 1) { // return a list of MIDI devices; just a list of names + pylist = PyList_New(0); + return pylist; + } + Py_INCREF (Py_None); + return Py_None; +} +#endif diff --git a/utility.c b/utility.c new file mode 100644 index 0000000..a9b5d08 --- /dev/null +++ b/utility.c @@ -0,0 +1,323 @@ +#include +#ifdef MS_WINDOWS +#include +#else +#include +#include +#endif +#include +#include "quisk.h" + +// Access to config file attributes. +// NOTE: These must be called only from the main (GUI) thread, +// not from the sound thread. + +int QuiskGetConfigInt(const char * name, int deflt) +{ // return deflt for failure. Accept int or float. + int res; + PyObject * attr; + if (!quisk_pyConfig || PyErr_Occurred()) { + return deflt; + } + attr = PyObject_GetAttrString(quisk_pyConfig, name); + if (attr) { + res = (int)PyInt_AsUnsignedLongMask(attr); // This works for floats too! + Py_DECREF(attr); + return res; // success + } + else { + PyErr_Clear(); + } + return deflt; // failure +} + +int QuiskGetConfigBoolean(const char * name, int deflt) // UNTESTED +{ // Return 1 for True, 0 for False. Return deflt for failure. + int res; + PyObject * attr; + if (!quisk_pyConfig || PyErr_Occurred()) { + return deflt; + } + attr = PyObject_GetAttrString(quisk_pyConfig, name); + if (attr) { + res = PyObject_IsTrue(attr); + Py_DECREF(attr); + return res; // success + } + else { + PyErr_Clear(); + } + return deflt; // failure +} + +double QuiskGetConfigDouble(const char * name, double deflt) +{ // return deflt for failure. Accept int or float. + double res; + PyObject * attr; + + if (!quisk_pyConfig || PyErr_Occurred()) + return deflt; + attr = PyObject_GetAttrString(quisk_pyConfig, name); + if (attr) { + res = PyFloat_AsDouble(attr); + Py_DECREF(attr); + return res; // success + } + else { + PyErr_Clear(); + } + return deflt; // failure +} + +char * QuiskGetConfigString(const char * name, char * deflt) +{ // Return the UTF-8 configuration string. Return deflt for failure. + char * res; + PyObject * attr; +#if PY_MAJOR_VERSION < 3 + static char retbuf[QUISK_SC_SIZE]; +#endif + + if (!quisk_pyConfig || PyErr_Occurred()) + return deflt; + attr = PyObject_GetAttrString(quisk_pyConfig, name); + if (attr) { +#if PY_MAJOR_VERSION >= 3 + res = (char *)PyUnicode_AsUTF8(attr); +#else + if (PyUnicode_Check(attr)) { + PyObject * pystr = PyUnicode_AsUTF8String(attr); + strMcpy(retbuf, PyString_AsString(pystr), QUISK_SC_SIZE); + retbuf[QUISK_SC_SIZE - 1] = 0; + res = retbuf; + Py_DECREF(pystr); + } + else { + res = PyString_AsString(attr); + } +#endif + Py_DECREF(attr); + if (res) + return res; // success + else + PyErr_Clear(); + } + else { + PyErr_Clear(); + } + return deflt; // failure +} + +double QuiskTimeSec(void) +{ // return time in seconds as a double +#ifdef MS_WINDOWS + FILETIME ft; + ULARGE_INTEGER ll; + + GetSystemTimeAsFileTime(&ft); + ll.LowPart = ft.dwLowDateTime; + ll.HighPart = ft.dwHighDateTime; + return (double)ll.QuadPart * 1.e-7; +#else + struct timeval tv; + + gettimeofday(&tv, NULL); + return (double)tv.tv_sec + tv.tv_usec * 1e-6; +#endif +} + +double QuiskDeltaSec(int timer) +{ // return the number of seconds since the last call for the timer. + // There are two timers. The "timer" is either 0 or 1. Call first and throw away the result. + static double time0[2] = {0, 0}; + double now; // in seconds + double delta; +#ifdef MS_WINDOWS + // Code contributed by Ben Cahill + static double timer_rate = 0; + LARGE_INTEGER L; + if (timer_rate == 0) { + if (QueryPerformanceFrequency(&L)) + timer_rate = (double)L.QuadPart; + else + timer_rate = 1.0; + } + if (QueryPerformanceCounter(&L)) + now = (double)L.QuadPart / timer_rate; + else + now = 0; +#else + struct timespec ts; +#ifdef CLOCK_MONOTONIC_RAW + if (clock_gettime(CLOCK_MONOTONIC_RAW, &ts) != 0) +#else + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) +#endif + return 0; + now = (double)ts.tv_sec + ts.tv_nsec * 1E-9; +#endif + if (timer < 0 || timer >= 2) + return 0; + if (now < time0[timer]) + now = time0[timer] = 0; + delta = now - time0[timer]; + time0[timer] = now; + return delta; +} + +void QuiskPrintTime(const char * str, int index) +{ // print the time and a message and the delta time for index 0 to 9 + double tm; + int i; + static double time0 = 0; + static double start_time[10]; +#ifdef MS_WINDOWS + static long long timer_rate = 0; + LARGE_INTEGER L; + if ( ! timer_rate) { + if (QueryPerformanceFrequency(&L)) + timer_rate = L.QuadPart; + else + timer_rate = 1; + } + if (QueryPerformanceCounter(&L)) + tm = (double)L.QuadPart / timer_rate; + else + tm = 0; +#else + struct timeval tv; + gettimeofday(&tv, NULL); + tm = (double)tv.tv_sec + tv.tv_usec * 1e-6; +#endif + if (index < -9 || index > 9) // error + return; + if (index < 0) { + start_time[ - index] = tm; + return; + } + if ( ! str) { // initialize + time0 = tm; + for (i = 0; i < 10; i++) + start_time[i] = tm; + return; + } + // print the time since startup, and the time since the last call + if (index > 0) { + if (str[0]) // print message and a newline + QuiskPrintf ("%12.6lf %9.3lf %9.3lf %s\n", + tm - time0, (tm - start_time[0])*1e3, (tm - start_time[index])*1e3, str); + else // no message; omit newline + QuiskPrintf ("%12.6lf %9.3lf %9.3lf ", + tm - time0, (tm - start_time[0])*1e3, (tm - start_time[index])*1e3); + } + else { + if (str[0]) // print message and a newline + QuiskPrintf ("%12.6lf %9.3lf %s\n", + tm - time0, (tm - start_time[0])*1e3, str); + else // no message; omit newline + QuiskPrintf ("%12.6lf %9.3lf ", + tm - time0, (tm - start_time[0])*1e3); + } + start_time[0] = tm; +} + +void QuiskSleepMicrosec(int usec) +{ +#ifdef MS_WINDOWS + int msec = (usec + 500) / 1000; // convert to milliseconds + if (msec < 1) + msec = 1; + Sleep(msec); +#else + struct timespec tspec; + tspec.tv_sec = usec / 1000000; + tspec.tv_nsec = (usec - tspec.tv_sec * 1000000) * 1000; + nanosleep(&tspec, NULL); +#endif +} + +void QuiskMeasureRate(const char * msg, int count, int index, int reset) +{ //measure the sample rate for index 0 to 9. If reset, reset the count and time at each print. + double tm; + static unsigned long total[10] = {0,0,0,0,0,0,0,0,0,0}; + static double time0[10] = {0,0,0,0,0,0,0,0,0,0}; + static double time_pr[10] = {0,0,0,0,0,0,0,0,0,0}; + + if ( ! msg) { // init + time0[index] = 0; + total[index] = 0; + return; + } + if (count && time0[index] == 0) { // init + time0[index] = time_pr[index] = QuiskTimeSec(); + total[index] = 0; + return; + } + if (time0[index] == 0) + return; + total[index] += count; + tm = QuiskTimeSec(); + if (tm > time_pr[index] + 10.0) { // time to print + time_pr[index] = tm; + QuiskPrintf("%s count %ld, time %.3lf, rate %.3lf\n", msg, total[index], tm - time0[index], total[index] / (tm - time0[index])); + if (reset) { + total[index] = 0; + time0[index] = tm; + } + } +} + +char * strMcpy(char * pDest, const char * pSrc, size_t sizeDest) +{ // replacement for strncpy() + size_t sizeCopy; + + sizeCopy = strnlen(pSrc, sizeDest - 1); + memcpy(pDest, pSrc, sizeCopy); + pDest[sizeCopy] = 0; + return pDest; +} + +#ifdef MS_WINDOWS +#define QP_BUF_DELTA 256 +PyObject * QuiskPrintf(char * format, ...) +{ // thread safe version of printf() needed by Windows + int length; + PyObject * py_string; + static int buf_size; // size of buffer + static int buf_strlen; // number of chars in buffer + static char * buffer = NULL; + va_list args; + + EnterCriticalSection(&QuiskCriticalSection); + if (buffer == NULL) { // initialize + buf_strlen = 0; + buf_size = QP_BUF_DELTA * 2; + buffer = malloc(buf_size); + } + if (format == NULL) { // return the string + py_string = PyUnicode_DecodeUTF8(buffer, buf_strlen, "replace"); + buf_strlen = 0; + LeaveCriticalSection(&QuiskCriticalSection); + return py_string; + } + if (buf_size - buf_strlen < QP_BUF_DELTA * 2) { // max addition is QP_BUF_DELTA + buf_size += QP_BUF_DELTA * 2; + buffer = realloc(buffer, buf_size); + } + va_start(args, format); + length = vsnprintf(buffer + buf_strlen, QP_BUF_DELTA, format, args); + if (length < 0) { + strcpy(buffer + buf_strlen, "Encoding error\n"); + buf_strlen = strlen(buffer); + } + else if (length > QP_BUF_DELTA - 1) { + buf_strlen = strlen(buffer); + } + else { +//printf("%s", buffer + buf_strlen); + buf_strlen += length; + } + va_end(args); + LeaveCriticalSection(&QuiskCriticalSection); + return NULL; +} +#endif