Even though I’m about to leave on a month-long holiday, I decided to spend a little time to put together a short project for element14’s Project 14 Acoustics Challenge. I was inspired by a Christmas video where someone used an oscilloscope’s arbitrary waveform generator to play a Christmas tune, made of sine wave tones. I wondered if it was possible to do any better, so I set to work abusing more test equipment.
The Basics of Digital Sound
As we all know, sounds are just pressure waves in air. We can easily generate sounds from electrical waves by passing it to a transducer such as a speaker that vibrates the air in the same fashion. Digital music stores the shape of these electrical waves as samples – a digital number representing the amplitude of the wave at periodic intervals (which is often compressed to save storage space). Thus, if you store these samples rapidly enough and reproduce them rapidly enough, we can record and reproduce basically all sounds. To a human, this can be achieved with about ~40000 to 48000 samples per second.
Knowing this, the digital to analog converter in a sound card, music player or smartphone is just taking these samples and producing the analog voltage corresponding to them to reconstruct the analog waveform. In essence, it is an arbitrary waveform generator (subject to certain constraints, especially frequency response, accuracy and DC levels).
Where else can you find an arbitrary waveform generator? Well, on the bench ... and in the Rohde & Schwarz RTM3004 Oscilloscope, as an option. Are you thinking what I’m thinking?
Making the Idea Happen
In order to make this idea happen, I needed to first understand what the equipment is capable of and then craft the necessary data files and code to command the instrument to play its performance.
Oscilloscope Settings
The first thing we have to know is how much memory the arbitrary waveform generator has. According to the datasheet, it claims 32k memory, but downloading some of the sample waveforms on the unit show that it is actually 32,768 samples long.
Knowing this, we also need to know what playout rates are available from this sample buffer. This is controlled by the frequency setting for the arbitrary waveform generator. Unfortunately, this is not the sample rate – i.e. setting it to 2Hz doesn’t play back two samples per second.
Frequency Setting (Hz) | Audio Sample Rate (Hz) | Duration of Buffer (s) |
---|---|---|
0.1 | 3276.8 | 10.0 |
0.2 | 6553.6 | 5.00 |
0.3 | 9830.4 | 3.33 |
0.4 | 13107.2 | 2.50 |
0.5 | 16384.0 | 2.00 |
0.6 | 19660.8 | 1.67 |
0.7 | 22937.6 | 1.43 |
0.8 | 26214.4 | 1.25 |
0.9 | 29491.2 | 1.11 |
1.0 | 32768.0 | 1.00 |
As we can see, the number of useful settings is quite limited. If we want high quality audio with a higher sample rate, the buffer becomes very short and needs to be reloaded very often. For comparison, a telephone call is ordinarily sampled at 8000Hz and an audio CD at 44100Hz. As a result, it seems that the setting of 0.3Hz/9830.4Hz/3.3s is a good compromise between length and quality, as there is another complication which is the time needed for the oscilloscope to process a load which would cause a “stutter” in-between each block. I did try at 0.5Hz/16384.0Hz/2s and found the oscilloscope could not keep up with the loading in time for playback – this is also because the .csv data files can be sizeable and have to be converted and scaled into the binary format used by the internal DAC.
Preparing the Audio
Prior to actually doing anything, we must first grab some audio and prepare it. Most of the audio files you might have might be compressed, sampled at a common rate (e.g. 8000/11025/16000/22050/24000/32000/44100/48000Hz) and have more than one channel (e.g. stereo).
To fix this, I used an audio editor (GoldWave, Audacity, etc) to first convert the file into a mono .WAV uncompressed file, then resampled the file to 9830Hz sample rate. The slight sample rate mismatch would result in the audio playing un-noticeably faster on the oscilloscope, but is a limitation of some audio editors when it comes to non-integer sample rates.
Once the audio is saved in this format, it can be processed to create .csv files suitable for the oscilloscope.
Processing the Audio
Before we begin, we must examine what the format of the files that are loaded on the oscilloscope look like. I downloaded one of the .csv files onboard and it looked like this:
time,value 0,0.0 1,0.0 2,0.0 3,0.0 4,0.0 5,0.0
This is easy enough to emulate – they are just pairs of time (0-32767) and value (-1.0 to 1.0). To do this, I decided to write a short Python tool using PySoundFile as the library to read .WAV files.
# Music Segment to CSV converter by Gough Lui - March 2020 # Expects .wav file, exports 32768 sample .CSVs in R&S RTM3004 format # Define filename root - rest is automatic. import soundfile as sf data, sr = sf.read("input.wav") print("Loaded file with sample rate of "+str(sr)+"Hz.") print("File has "+str(len(data))+" samples, thus need "+str(int(len(data)/32768)+1)+" segments.") filename_base = "voic" filename_count = 0 # Starting Count sample = 0 # Starting Sample Number for m in range (0,int(len(data)/32768)+1) : print("Writing File "+filename_base+str(filename_count)+".csv") f = open(filename_base+str(filename_count)+".csv","w") f.write("time,value\n") for i in range (0,32768) : f.write(str(i)+","+str(data[sample])+"\n") if sample != len(data)-1 : sample=sample+1 else : break; filename_count=filename_count+1 print("Done!")
The tool does have a subtle bug – more about this in the video and conclusion, but it makes a whole batch of files with a root file name followed by an incrementing number.
Loading the Data Files
Unfortunately, the RTM3004 also seems limited in some extent when it comes to loading the data files. I was expecting that I could perhaps load the arbitrary waveform memory data directly over SCPI remote control (over LAN), but there doesn’t seem to be a command for this at this time. Instead, it expects to read files stored on a USB flash drive or its internal memory, referenced by filename.
To accommodate for this, I decided to load the .csv files to a USB flash drive attached to the front panel. The next issue is that of playback. The unit doesn’t seem to provide any confirmation when the generator has played a buffer full of audio (*OPC? does not seem to do it), thus it doesn’t seem possible to actually sequence smooth playback, especially due to command execution time, load time from USB and some jitter in command timings. Instead, I decided to blindly fire commands from my desktop to the oscilloscope at roughly the right timing (slightly earlier, to compensate for overheads). This unfortunately means that stutter-free playback is not a possibility – but I suppose this was always expected as there isn’t any streaming mode or ping-pong buffer capabilities as real sound-cards often do. But this is not unexpected – we are abusing the arbitrary waveform generator, after all.
The code for playing back files using pyvisa looks like this:
# Music Player using Arb Waveform Gen on RTM3004 by Gough Lui - March 2020 # No warranties - you must modify the code to match your instrument VISA identifier and channel import visa import time filename_base = "SYNER" filename_count = 0 # Starting Count filename_max = 82 # Number of segments resource_manager = visa.ResourceManager() ins_rtm3004 = resource_manager.open_resource("TCPIP0::192.168.80.5::INST0::INSTR") print("Connected to:" + "\n" + ins_rtm3004.query("*IDN?")) input("Ready to Begin - Press Enter to Continue") ins_rtm3004.write("WGEN:OUTP:LOAD R50") ins_rtm3004.write("WGEN:VOLT 5") ins_rtm3004.write("WGEN:VOLT:OFFS 0") ins_rtm3004.write("WGEN:FREQ 5.0E-1") ins_rtm3004.write("WGEN:FUNC ARB") ins_rtm3004.query("*OPC?") ins_rtm3004.write("WGEN:ARB:NAME \"/USB_FRONT/"+filename_base+str(filename_count)+".CSV\"") ins_rtm3004.write("WGEN:ARB:OPEN") ins_rtm3004.write("WGEN:OUTP ON") while filename_count < filename_max : time.sleep(1) filename_count=filename_count+1 ins_rtm3004.write("WGEN:ARB:NAME \"/USB_FRONT/"+filename_base+str(filename_count)+".CSV\"") ins_rtm3004.write("WGEN:ARB:OPEN") ins_rtm3004.write("WGEN:VOLT 5") ins_rtm3004.write("WGEN:VOLT:OFFS 0") print("Done! Closing down ...") ins_rtm3004.write("WGEN:OUTP OFF") ins_rtm3004.close()
The code does have a quirk – one of which is the need to keep setting the output voltage and offset values. I found that if I did not do this, it seems to revert to some random previously-used value, which seems to be a firmware quirk of the device.
Making Sounds
To convert the created arbitrary waveform voltage to sound, I used a speaker. The particular speaker used was salvaged from an old cordless telephone and had an impedance of 40Ω. This is much friendlier to the arbitrary waveform generator output which was set to low impedance output of 50Ω. Had I used a regular 4 or 8Ω speaker, that would have likely severely overloaded the output which could potentially lead to damage. Unfortunately, without an amplifier of any sort, the sound output is not particularly loud … but I can compensate for that by placing the recorder right in front of the speaker.
Video
Above is a video of the Rohde & Schwarz RTM3004’s arbitrary waveform generator being abused in this way. A total of 116 .csv files make up the audio played in the video and that costs 91MiB of storage on the USB flash drive.
Conclusion
Curious as to whether oscilloscope music could be better than just a bunch of tones, the answer is yes, sort of. By abusing a limited-memory arbitrary waveform generator as an audio DAC, we can get audio from the unit, but with a stutter between every block of samples. Such devices are not optimised for streaming and constant reloading of samples, thus this was an expected outcome, but I suppose it was still pretty interesting to try it out for myself.
As mentioned in the video, there is a bug with the .csv generator code which is that it doesn’t pad a partial chunk of samples up to 32,768. This results in the last chunk being played at the wrong rate, as the arbitrary waveform generator’s frequency setting represents the rate at which whole chunks are played and not their individual samples. As a result, a “shorter” chunk and a regular full chunk occupy the same time, hence slowing down to a growl. That’s easily fixed in the code just by outputting zero values for the final partial chunk – but I’ll leave this as an exercise for the reader to perform.
Top Comments