How to produce precisely-timed tone and silence?
Asked Answered
T

4

22

I have a C# project that plays Morse code for RSS feeds. I write it using Managed DirectX, only to discover that Managed DirectX is old and deprecated. The task I have is to play pure sine wave bursts interspersed with silence periods (the code) which are precisely timed as to their duration. I need to be able to call a function which plays a pure tone for so many milliseconds, then Thread.Sleep() then play another, etc. At its fastest, the tones and spaces can be as short as 40ms.

It's working quite well in Managed DirectX. To get the precisely timed tone I create 1 sec. of sine wave into a secondary buffer, then to play a tone of a certain duration I seek forward to within x milliseconds of the end of the buffer then play.

I've tried System.Media.SoundPlayer. It's a loser [edit - see my answer below] because you have to Play(), Sleep(), then Stop() for arbitrary tone lengths. The result is a tone that is too long, variable by CPU load. It takes an indeterminate amount of time to actually stop the tone.

I then embarked on a lengthy attempt to use NAudio 1.3. I ended up with a memory resident stream providing the tone data, and again seeking forward leaving the desired length of tone remaining in the stream, then playing. This worked OK on the DirectSoundOut class for a while (see below) but the WaveOut class quickly dies with an internal assert saying that buffers are still on the queue despite PlayerStopped = true. This is odd since I play to the end then put a wait of the same duration between the end of the tone and the start of the next. You'd think that 80ms after starting Play of a 40 ms tone that it wouldn't have buffers on the queue.

DirectSoundOut works well for a while, but its problem is that for every tone burst Play() it spins off a separate thread. Eventually (5 min or so) it just stops working. You can see thread after thread after thread exiting in the Output window while running the project in VS2008 IDE. I don't create new objects during playing, I just Seek() the tone stream then call Play() over and over, so I don't think it's a problem with orphaned buffers/whatever piling up till it's choked.

I'm out of patience on this one, so I'm asking in the hopes that someone here has faced a similar requirement and can steer me in a direction with a likely solution.

Thury answered 27/4, 2010 at 21:33 Comment(4)
I just slightly edited the post and it already has 4 votes? Wow.Thury
It's an interesting application, and you ask some good questions. So yeah, upvote.Ibanez
If you have it working in Managed DirectX (MDX) why is it a problem that it is no longer supported by Microsoft? Just because it is not supported doesn't mean you can't use it. Any machine that has .NET 2 and DirectX 9 installed should continue to run MDX applications no problems.Tripos
That may be the route I go on this. It's a freebie "toy" app and I'm limited on the time I can spend fiddling with it :-)Thury
T
8

I can't believe it... I went back to System.Media.SoundPlayer and got it to do just what I want... no giant dependency library with 95% unused code and/or quirks waiting to be discovered :-). Furthermore, it runs on MacOSX under Mono (2.6)!!! [wrong - no sound, will ask separate question]

I used a MemoryStream and BinaryWriter to crib a WAV file, complete with the RIFF header and chunking. No "fact" chunk needed, this is 16-bit samples at 44100Hz. So now I have a MemoryStream with 1000ms of samples in it, and wrapped by a BinaryReader.

In a RIFF file there are two 4-byte/32-bit lengths, the "overall" length which is 4 bytes into the stream (right after "RIFF" in ASCII), and a "data" length just before the sample data bytes. My strategy was to seek in the stream and use the BinaryWriter to alter the two lengths to fool the SoundPlayer into thinking the audio stream is just the length/duration I want, then Play() it. Next time, the duration is different, so once again overwrite the lengths in the MemoryStream with the BinaryWriter, Flush() it and once again call Play().

When I tried this, I couldn't get the SoundPlayer to see the changes to the stream, even if I set its Stream property. I was forced to create a new SoundPlayer... every 40 milliseconds??? No.

Well I want back to that code today and started looking at the SoundPlayer members. I saw "SoundLocation" and read it. There it said that a side effect of setting SoundLocation would be to null the Stream property, and vice versa for Stream. So I added a line of code to set the SOundLocation property to something bogus, "x", then set the Stream property to my (just modified) MemoryStream. Damn if it didn't pick that up and play a tone precisely as long as I asked for. There don't seem to be any crazy side effects like dead time afterward or increasing memory, or ??? It does take 1-2 milliseconds to do that tweaking of the WAV stream and then load/start the player, but it's very small and the price is right!

I also implemented a Frequency property which re-generates the samples and uses the Seek/BinaryWriter trick to overlay the old data in the RIFF/WAV MemoryStream with the same number of samples but for a different frequency, and again did the same thing for an Amplitude property.

This project is on SourceForge. You can get to the C# code for this hack in SPTones.CS from this page in the SVN browser. Thanks to everyone who provided info on this, including @arke whose thinking was close to mine. I do appreciate it.

Thury answered 28/4, 2010 at 3:46 Comment(2)
+1 for going back and describing what you did, even though you don't need any more help. You can mark your own answer as the correct one on stackoverflow. If you want.Stanwood
Casey -- I'll do that tomorrow ... StackOverflow says you have to wait 2 days before being able to mark your own answer to your own question to be the "right" answer ... not surprising :-)Thury
I
3

It's best to just generate the sine waves and silence together into a buffer which you play. That is, always play something, but write whatever you need next into that buffer.

You know the samplerate, and given the samplerate, you can calculate the amount of samples you need to write.

uint numSamples = timeWantedInSeconds * sampleRate;

That's the amount of samples you need to generate a sine wave or silence, whichever. Then just fill the buffer as needed. That way, you get the most accurate possible timing.

Ixia answered 27/4, 2010 at 21:44 Comment(2)
Once again, arke -- thanks! This may be the "right" way to go. It will be complex though because American Morse Code has "nuances" in the timing. I already have all of this worked out in code that supplies a series of millisecond values for the "mark" and "space". That's why I'm looking for a way to produce arbitrary tone and space lengths. That's what I'm doing with Managed DirectX now and it's virtually perfect.Thury
I'm looking at ASIO.NET right now codeproject.com/KB/audio-video/Asio_Net.aspxThury
C
1

Try using XNA.

You will have to provide a file, or a stream to a static tone, that you can loop. You can then change the pitch and volume of that tone.

Since XNA is made for games, it will have no problem at all with 40 ms delays.

Cite answered 27/4, 2010 at 21:37 Comment(6)
So XNA is usable from C#? I didn't know that. Is there some big library that needs to be installed by my users to make it work? I'll look while I await your answer. Thanks!!Thury
Yes yes yes! weblogs.asp.net/scottgu/archive/2006/12/19/…Finisterre
XNA will have the same problem. Especially on Windows XP, but also likely on Vista and up depending on the drivers.Ixia
It seems that in XNA you can only play from a file, but "buffering" is in an upcoming version? And thanks arke - I can't afford another failed experiment. The last bit of advice ("use NAudio") cost me 2 days including trudging through the library source to discover a basic limitation.Thury
As I told in my answer, you can only play static files, HOWEVER you have full control on pitch, volume play and stop. It is very fast since it is made for games. The windows media lib loads and then play the file, hence the porr performance. XNA lets you preload the file then play it and apply effects as you see fit.Cite
Since you are only trying to play morse, it will be more than enough.Cite
R
1

It should be pretty easy to convert from ManagedDX to SlimDX ...

Edit: What stops you, btw, just pre-generating 'n' samples of sine wave? (Where n is the closest to the number of milliseconds you want). It really doesn't take all that long to generate the data. Further than that if you have a 22Khz buffer and you want the final 100 samples why don't you just submit 'buffer + 21950' and set the buffer length to 100 samples?

Resht answered 27/4, 2010 at 22:8 Comment(2)
That's what I am essentially doing, just setting the secondaryBuffer's start position to within 'n' milliseconds of the end, then playing. This works great in Managed DirectX. I'm looking at ASIO right now. With ASIO4ALL it looks very light weight, and uses WDM device for output.Thury
Oh, and I looked at SlimDX and may look again. It needs too much glue, and it's a commercial product that has a free license. I don't need all that drawing/3D stuff. But I didn't give it a good shake. There's a limit to the time I can spend on this, and the redistribution limits on SlimDX look like a disadvantage.Thury

© 2022 - 2024 — McMap. All rights reserved.