I would like to play a specific range[startTime, endTime] of a sound file, is there an efficient way to do it ? I tried to setLoopPoints and setDelay but I couldn’t make them work. Of course, I could monitor the playback through callback and stop the playing, but I think there might be a better way to do it.
Per the documentation, after playing a sound its channel should be invalid, I don’t see it happens when I call hasHandle on a channel the handle seems valid. Does it make any sense?
You can scrub to a position of a playing Sound by calling Channel::setPosition on it’s Channel handle. For setLoopPoints to work the sound’s mode will need to be set to FMOD_LOOP_NORMAL or FMOD_LOOP_BIDI.
Where in the docs does it say after playing a sound the channel should be invalid? After playing a sound the channel should be valid if playSound was succesful.
Channel::setPosition will work on the “start”, but the problem is how to accurately end the playback on endTime; I thought about starting some timer, but would like to avoid managing multiple timers. Is there some more elegant way to deal with this? Maybe I could register to some callbacks? I did check the channel callback, didn’t look like it could help.
I saw it in the below book… I think I saw it also in the documentation, but not sure.
I believe setLoopPoints should be sample accurate, and then you would just need to set the loop count to 0 with Channel::setLoopCount to make sure it only plays once.
That book is correct, the channel will be invalid only after the sound has stopped or completed it’s playback, but will be valid until either of those two things occur.
Well, it doesn’t seem like setLoopPoints is doing the trick. It does loop, but after the count is reached, the sound is playing without stopping. It looks like I will have to monitor the playback Any idea?
Yeah, that’s what I meant - that post-playback(complete/stopped) the channel’s handle cannot be re-used and should be invalid? I don’t see this happening… how do I check if the handle is valid?
You are right, sorry for the oversight there. I think your original solution of having callbacks is the best way to go, though I am finding it is off by 5-30ms which isn’t ideal. I have added a task to our backlog to implement a call for sample-accurate playback within a given time range.
Here is a rough example of you can implement such a callback:
FMOD_RESULT F_CALLBACK SyncCallback(FMOD_CHANNELCONTROL* channelcontrol, FMOD_CHANNELCONTROL_TYPE controltype, FMOD_CHANNELCONTROL_CALLBACK_TYPE callbacktype, void* commanddata1, void* commanddata2)
{
if (callbacktype == FMOD_CHANNELCONTROL_CALLBACK_SYNCPOINT)
{
FMOD::ChannelGroup *cc = (FMOD::ChannelGroup*)channelcontrol;
return cc->stop();
}
return FMOD_OK;
}
FMOD_RESULT FMOD_Play_Sound_Range(FMOD::System *system, FMOD::Sound *sound, int start, FMOD_TIMEUNIT starttimeunit, int end, FMOD_TIMEUNIT endtimeunit)
{
FMOD_RESULT result;
FMOD_SYNCPOINT *endpoint;
FMOD::Channel *channel;
int numsyncpoints;
// Remove existing syncpoints
result = sound->getNumSyncPoints(&numsyncpoints);
ERRCHECK(result);
for (int i = 0; i < numsyncpoints; ++i)
{
FMOD_SYNCPOINT* syncpoint;
result = sound->getSyncPoint(i, &syncpoint);
ERRCHECK(result);
result = sound->deleteSyncPoint(syncpoint);
ERRCHECK(result);
}
// Add a single syncpoint for the end time
result = sound->addSyncPoint(end, FMOD_TIMEUNIT_MS, "END", &endpoint);
ERRCHECK(result);
result = system->playSound(sound, 0, false, &channel);
ERRCHECK(result);
// Set start time
result = channel->setPosition(start, FMOD_TIMEUNIT_MS);
ERRCHECK(result);
result = channel->setCallback(SyncCallback);
ERRCHECK(result);
return FMOD_OK;
}
The easiest way is to try and call something on the handle I suppose, if it returns FMOD_ERR_INVALID_HANDLE.
void SetChannelRange(FMOD::ChannelGroup *channel, int start, int end)
{
int duration = stop - start;
int sampleRate;
unsigned long long time;
result = system->getSoftwareFormat(&sampleRate, 0, 0);
ERRCHECK(result);
result = channel->getDSPClock(0, &time);
ERRCHECK(result);
result = channel->setPosition(start * sampleRate, FMOD_TIMEUNIT_PCM);
ERRCHECK(result);
result = channel->setDelay(0, time + duration * sampleRate);
ERRCHECK(result);
}
As for the callback, I can see you are using C#, does your callback signature look like this?
Same signature,
I don’t have the MonoPInvokeCallback though.
this.hSystem.createChannelGroup(groupName, out this.channelGroup);
this.hSystem.playSound(this.hSound, this.channelGroup, true, out this.currentChannel);
The above is how group channel being created. Is this how it should be done? Basically in c#, fmod’s API forces me to provide channel group. The callback is set on “this.currentChannel”.
Am I missing something?
Any callbacks into FMOD require the MonoPInvokeCallback attribute since the callback will be called by native code. MonoPInvokeCallback also needs to be static.
Yes you can create a new ChannelGroup for your sound, or:
playSound(sound, new ChannelGroup(), false, out Channel channel);
To give it an invalid ChannelGroup which will make it playback on the default master channel group.
If you are just using .NET without Mono/Unity I don’t think you need any of the AOT stuff. Here is a basic .NET program showing how to set a CHANNELCONTROL_CALLBACK.
private static CHANNELCONTROL_CALLBACK channelControlCallback;
private static RESULT ChannelControlCallbackFunc(IntPtr channelcontrol, FMOD.CHANNELCONTROL_TYPE controltype, FMOD.CHANNELCONTROL_CALLBACK_TYPE callbacktype, IntPtr commanddata1, IntPtr commanddata2)
{
if (callbacktype == CHANNELCONTROL_CALLBACK_TYPE.SYNCPOINT)
{
Channel cc = new Channel(channelcontrol);
return cc.stop();
}
return RESULT.OK;
}
private static RESULT PlaySoundRange(FMOD.System system, Sound sound, uint start, TIMEUNIT starttimeunit, uint end, TIMEUNIT endtimeunit)
{
RESULT result;
IntPtr syncpoint, endpoint;
Channel channel;
int numsyncpoints;
// Remove existing syncpoints
result = sound.getNumSyncPoints(out numsyncpoints);
ERRCHECK(result);
for (int i = 0; i < numsyncpoints; ++i)
{
result = sound.getSyncPoint(i, out syncpoint);
ERRCHECK(result);
result = sound.deleteSyncPoint(syncpoint);
ERRCHECK(result);
}
// Add a single syncpoint for the end time
result = sound.addSyncPoint(end, TIMEUNIT.MS, "END", out endpoint);
ERRCHECK(result);
result = system.playSound(sound, new ChannelGroup(), false, out channel);
ERRCHECK(result);
// Set start time
result = channel.setPosition(start, TIMEUNIT.MS);
ERRCHECK(result);
result = channel.setCallback(channelControlCallback);
ERRCHECK(result);
return RESULT.OK;
}
[STAThread]
static void Main(string[] args)
{
channelControlCallback = new CHANNELCONTROL_CALLBACK(ChannelControlCallbackFunc);
ERRCHECK(Factory.System_Create(out FMOD.System system));
ERRCHECK(system.init(32, INITFLAGS.NORMAL, IntPtr.Zero));
ERRCHECK(system.createSound("music_48kbps.wav", MODE.DEFAULT, out Sound sound));
PlaySoundRange(system, sound, 2000, TIMEUNIT.MS, 4000, TIMEUNIT.MS); // Play 2-4 seconds
do
{
ERRCHECK(system.update());
System.Threading.Thread.Sleep(50);
} while (!exit);
ERRCHECK(sound.release());
ERRCHECK(system.release());
}
#region Details
static bool exit = false;
static void CurrentDomain_ProcessExit(object sender, EventArgs e)
{
exit = true;
}
public static void ERRCHECK(FMOD.RESULT result)
{
if (result != FMOD.RESULT.OK)
{
Console.WriteLine(result);
}
}
#endregion
yes, exactly my code… I don’t loop and call “system.update” repeatedly though. I guess that’s the problem? I have a UI thread, so I guess I need a dedicated thread for fmod in this case?
It doesn’t neccessarily need to be on a different thread, you can just run it on your main thread. Keeping it separate from the UI is definitely a good idea though.