I've successfully gotten iOS to play a .mid (midi) file with a soundfont sample using the following code:
-(void) playMusic:(NSString*) name
{
NSString *presetURLPath = [[NSBundle mainBundle] pathForResource:#"GortsMiniPianoJ1" ofType:#"SF2"];
NSURL * presetURL = [NSURL fileURLWithPath:presetURLPath];
[self loadFromDLSOrSoundFont: (NSURL *)presetURL withPatch: (int)3];
NSString *midiFilePath = [[NSBundle mainBundle] pathForResource:name ofType:#"mid"];
NSURL * midiFileURL = [NSURL fileURLWithPath:midiFilePath];
NewMusicPlayer(&musicPlayer);
if (NewMusicSequence(&musicSequence) != noErr)
{
[NSException raise:#"play" format:#"Can't create MusicSequence"];
}
if(MusicSequenceFileLoad(musicSequence, (CFURLRef)midiFileURL, 0, 0 != noErr))
{
[NSException raise:#"play" format:#"Can't load MusicSequence"];
}
MusicPlayerSetSequence(musicPlayer, musicSequence);
MusicSequenceSetAUGraph(musicSequence, _processingGraph);
MusicPlayerPreroll(musicPlayer);
MusicPlayerStart(musicPlayer);
}
However, the problem comes when I then try to play a second file when the first is still playing.
I've tried many variations. Firstly, the above code will play both tracks simultaneously. Or, I've tried:
DisposeMusicPlayer(musicPlayer);
DisposeMusicSequence(musicSequence);
Before the NewMusicPlayer(&musicPlayer), but this produces a weird version of the tune with only sporadic notes being played.
I'd love to simply call this method, and the next track to be played.
Ok, I found the answer on how to properly dispose of a MusicPlayer and MusicSequence.
-(void) stop
{
OSStatus result = noErr;
result = MusicPlayerStop(musicPlayer);
UInt32 trackCount;
MusicSequenceGetTrackCount(musicSequence, &trackCount);
MusicTrack track;
for(int i=0;i<trackCount;i++)
{
MusicSequenceGetIndTrack (musicSequence, i, &track);
result = MusicSequenceDisposeTrack(musicSequence, track);
}
result = DisposeMusicPlayer(musicPlayer);
result = DisposeMusicSequence(musicSequence);
result = DisposeAUGraph(_processingGraph);
}
Related
I'm successfully loading a Sound Font (.sf2) to a MusicSequence when the MusicSequence it's loaded by file (.mid) like the following:
//the interesting code ...
NSString *midiFilePath = [[NSBundle mainBundle]
pathForResource:#"song"
ofType:#"mid"];
// Create a new URL which points to the MIDI file
NSURL * midiFileURL = [NSURL fileURLWithPath:filePath];
MusicSequenceFileLoad(s, (__bridge CFURLRef) midiFileURL, 0, 0);
// Create a new music player
MusicPlayer p;
// Initialise the music player
NewMusicPlayer(&p);
// ************* Set the endpoint of the sequence to be our virtual endpoint
MusicSequenceSetMIDIEndpoint(s, virtualEndpoint);
// Load the sound font from file
NSURL *presetURL = [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:#"Full Grand Piano" ofType:#"sf2"]];
// Initialise the sound font
[self loadFromDLSOrSoundFont: (NSURL *)presetURL withPatch: (int)10];
// Load the sequence into the music player
MusicPlayerSetSequence(p, s);
// Called to do some MusicPlayer setup. This just
// reduces latency when MusicPlayerStart is called
MusicPlayerPreroll(p);
// Starts the music playing
MusicPlayerStart(p);
//code continues here ...
-(OSStatus) loadFromDLSOrSoundFont: (NSURL *)bankURL withPatch: (int)presetNumber {
OSStatus result = noErr;
// fill out a bank preset data structure
AUSamplerBankPresetData bpdata;
bpdata.bankURL = (__bridge CFURLRef) bankURL;
bpdata.bankMSB = kAUSampler_DefaultMelodicBankMSB;
bpdata.bankLSB = kAUSampler_DefaultBankLSB;
bpdata.presetID = (UInt8) presetNumber;
// set the kAUSamplerProperty_LoadPresetFromBank property
result = AudioUnitSetProperty(self.samplerUnit,
kAUSamplerProperty_LoadPresetFromBank,
kAudioUnitScope_Global,
0,
&bpdata,
sizeof(bpdata));
// check for errors
NSCAssert (result == noErr,
#"Unable to set the preset property on the Sampler. Error code:%d '%.4s'",
(int) result,
(const char *)&result);
return result;
}
But If I want to apply the same to a MusicSequence with a MusicTrack build with MIDINoteMessages:
//the interesting code here
MusicSequenceNewTrack(musicSequence, &musicTrack);
MusicSequenceGetIndTrack(musicSequence, 0, &(musicTrack));
MIDINoteMessage aMessage;
aMessage.channel = 1;
aMessage.duration = 0.3f;
aMessage.velocity = 200;
for(int i=0; i<numerator; ++i)
{
if (i==0) {
aMessage.note = 80;
}else {
aMessage.note = 60;
}
MusicTrackNewMIDINoteEvent(musicTrack, i, &aMessage);
}
MusicSequenceSetMIDIEndpoint(musicSequence, virtualEndpoint);
// Load the sound font from file
NSURL *presetURL = [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:#"Full Grand Piano" ofType:#"sf2"]];
// Initialise the sound font
[self loadFromDLSOrSoundFont: (NSURL *)presetURL withPatch: (int)10];
//code continues here
I'm getting a (lldb) error. Any idea how to solve this?
This is the sound font relevant code from my github project. This loads the sound font into your sampler unit. If you need more on setting up the augraph see the project or read this post.
func loadSF2Preset(preset:UInt8) {
// This is the MuseCore soundfont. Change it to the one you have.
if let bankURL = NSBundle.mainBundle().URLForResource("GeneralUser GS MuseScore v1.442", withExtension: "sf2") {
var instdata = AUSamplerInstrumentData(fileURL: Unmanaged.passUnretained(bankURL),
instrumentType: UInt8(kInstrumentType_DLSPreset),
bankMSB: UInt8(kAUSampler_DefaultMelodicBankMSB),
bankLSB: UInt8(kAUSampler_DefaultBankLSB),
presetID: preset)
var status = AudioUnitSetProperty(
self.samplerUnit,
AudioUnitPropertyID(kAUSamplerProperty_LoadInstrument),
AudioUnitScope(kAudioUnitScope_Global),
0,
&instdata,
UInt32(sizeof(AUSamplerInstrumentData)))
CheckError(status)
}
}
You associate the augraph with the MusicSequence like this
MusicSequenceSetAUGraph(musicSequence, self.processingGraph)
In my blog post on MIDI callbacks I've included a github project that has code to create a MusicSequence on the fly as you are trying to do.
I am creating MIDI sounds with MusicDeviceMIDIEvent and it's perfectly fine, I use some aupreset files I've created. One for each instrument.
My code is basically the example from apple: LoadPresetDemo. All I use is:
- (void)loadPreset:(id)sender instrumentName:(const char *)instrumentName {
NSString *presetURL1 = [NSString stringWithCString:instrumentName encoding:NSUTF8StringEncoding];
NSString *path = [[NSBundle mainBundle] pathForResource:presetURL1 ofType:#"aupreset"];
NSURL *presetURL = [[NSURL alloc] initFileURLWithPath: path];
if (presetURL) {
NSLog(#"Attempting to load preset '%#'\n", [presetURL description]);
}
else {
NSLog(#"COULD NOT GET PRESET PATH!");
}
[self loadSynthFromPresetURL: presetURL];
}
To load my aupreset file, then:
- (void) noteOn:(id)sender midiNumber:(int)midiNumber {
UInt32 noteNum = midiNumber;
UInt32 onVelocity = 127;
UInt32 noteCommand = kMIDIMessage_NoteOn << 4 | 0;
OSStatus result = noErr;
require_noerr (result = MusicDeviceMIDIEvent (self.samplerUnit, noteCommand, noteNum, onVelocity, 0), logTheError);
logTheError:
if (result != noErr) NSLog (#"Unable to start playing the low note. Error code: %d '%.4s'\n", (int) result, (const char *)&result);
}
to play the note of the previously loaded aupreset, and:
- (void) noteOff:(id)sender midiNumber:(int)midiNumber
when I want it to stop.
Now I would like to play one note of each instrument simultaneously. What is the easiest way of doing this?
I have been ploughing along with my learning of objective c and have got my app doing more or less everything I want it to do. I have the correct audio playing when certain buttons are pressed.
I have run in to some difficulty when localizing my audio file.
I have localized other elements of my app with no issue, for example I have used different images and button backgrounds and text by localizing in the same way but when I localize audio i run in to some issues and I can not find a solutions online as I normally do.
So as I said the issue only occurs when the file is localized, it effects the original language (where it was working before localizing the file) and the other languages (in this case French)
So the code I am using on my button is:
- (IBAction)domesticButtonClicked:(id)sender
{
NSString* resourcePath = [[NSBundle mainBundle] resourcePath];
resourcePath = [resourcePath stringByAppendingString:#"/localize-test.wav"];
// NSLog(#"Path to play: %#", resourcePath);
NSError* err;
//Initialize our player pointing to the path to our resource
domesticSound = [[AVAudioPlayer alloc] initWithContentsOfURL:
[NSURL fileURLWithPath:resourcePath] error:&err];
if( err ){
//bail!
NSLog(#"Failed with reason: %#", [err localizedDescription]);
}
else{
//set our delegate and begin playback
domesticSound.delegate = self;
[domesticSound play];
domesticSound.numberOfLoops = 0;
domesticSound.currentTime = 0;
domesticSound.volume = 1.0;
}
}
}
So when this runs, instead of it playing the sound as it should I see the following in my output:
Failed with reason: The operation couldn’t be completed. (OSStatus error 2003334207.)
Can anyone shed any light on this for me?
Thanks in advance.
EDIT:
Thanks Marcus Adams, what you suggested does work but I can not use it on some buttons as i can not declare it every time.
So the reason the audio file is declared that way is so the audio file can be altered depending on the number of times the button has been clicked. Maybe this will help in getting my issue resolved.
- (IBAction)domestic03ButtonClicked:(id)sender
{
static int imageNumber = 0;
NSString* resourcePath = [[NSBundle mainBundle] resourcePath];
if (imageNumber == 0) {
domestic03ImageContainer.image = domestic300;
resourcePath = [resourcePath stringByAppendingString:#"/smb_coin1.wav"];
imageNumber++;
}
else if (imageNumber == 1) {
domestic03ImageContainer.image = domestic301;
resourcePath = [resourcePath stringByAppendingString:#"/smb_coin2.wav"];
imageNumber++;
}
else if (imageNumber == 2) {
domestic03ImageContainer.image = domestic302;
resourcePath = [resourcePath stringByAppendingString:#"/smb_coin3.wav"];
imageNumber++;
}
else if (imageNumber == 3) {
domestic03ImageContainer.image = domestic303;
resourcePath = [resourcePath stringByAppendingString:#"/smb_coin4.wav"];
imageNumber++;
}
else if (imageNumber == 4) {
domestic03ImageContainer.image = domestic304;
resourcePath = [resourcePath stringByAppendingString:#"/smb_coin5.wav"];
imageNumber = 0;
}
// NSLog(#"Path to play: %#", resourcePath);
NSError* err;
//Initialize our player pointing to the path to our resource
domesticSound = [[AVAudioPlayer alloc] initWithContentsOfURL:
[NSURL fileURLWithPath:resourcePath] error:&err];
if( err ){
//bail!
NSLog(#"Failed with reason: %#", [err localizedDescription]);
}
else{
//set our delegate and begin playback
domesticSound.delegate = self;
[domesticSound play];
domesticSound.numberOfLoops = 0;
domesticSound.currentTime = 0;
domesticSound.volume = 1.0;
}
domestic03ImageContainer.Alpha = 1;
}
You should be able to use just this for your path, assuming you've put the resource directly in the proper localized folder:
NSString* resourcePath = [[NSBundle mainBundle] pathForResource:#"localize-test"
ofType:#"wav"];
You should only check err if domesticSound == nil. This is generally true for APIs that take an NSError** in/out argument. (err is not guaranteed to be written to upon success.)
You should also initialize err to nil!
i'm using OpenAL to play background music and sound effects in a game, the code to play background music is this;
- (void) playSoundBack
{
alGenSources(1, &sourceIDBack);
NSString *audioFilePath = [[NSBundle mainBundle] pathForResource:#"music" ofType:#"wav"];
NSURL *audioFileURL = [NSURL fileURLWithPath:audioFilePath];
AudioFileID afid;
OSStatus openAudioFileResult = AudioFileOpenURL((__bridge CFURLRef)audioFileURL, kAudioFileReadPermission, 0, &afid);
if (0 != openAudioFileResult)
{
NSLog(#"An error occurred when attempting to open the audio file %#: %ld", audioFilePath, openAudioFileResult);
return;
}
UInt64 audioDataByteCount = 0;
UInt32 propertySize = sizeof(audioDataByteCount);
OSStatus getSizeResult = AudioFileGetProperty(afid, kAudioFilePropertyAudioDataByteCount, &propertySize, &audioDataByteCount);
if (0 != getSizeResult)
{
NSLog(#"An error occurred when attempting to determine the size of audio file %#: %ld", audioFilePath, getSizeResult);
}
UInt32 bytesRead = (UInt32)audioDataByteCount;
void *audioData = malloc(bytesRead);
OSStatus readBytesResult = AudioFileReadBytes(afid, false, 0, &bytesRead, audioData);
if (0 != readBytesResult)
{
NSLog(#"An error occurred when attempting to read data from audio file %#: %ld", audioFilePath, readBytesResult);
}
AudioFileClose(afid);
ALuint outputBuffer;
alGenBuffers(1, &outputBuffer);
alBufferData(outputBuffer, AL_FORMAT_STEREO16, audioData, bytesRead, 44100);
if (audioData)
{
free(audioData);
audioData = NULL;
}
alSourcef(sourceIDBack, AL_GAIN, 0.1f);
alSourcei(sourceIDBack, AL_BUFFER, outputBuffer);
alSourcei(sourceIDBack, AL_LOOPING, AL_TRUE);
alSourcePlay(sourceIDBack);
}
With another similar block of code i play some effects during the game. All the sound are played but i have a problem with volumes... The background music volume change and it seems not an absolute value... When the other effects are playing the background music volume is low as i have set in with AL_GAIN,but when no other effect is playing the background music sounds too loud (or at least this is my sensation...). Why the music has this behaviour? The volume setting like 0.1 are relative? It is possible to set a fixed and universal value?
I want to repeat the sounds that are send to this method?
- (void) playSound:(CFURLRef)soundFileURLRef {
SystemSoundID soundID;
OSStatus errorCode = AudioServicesCreateSystemSoundID(soundFileURLRef, &soundID);
if (errorCode != 0) {
}else{
AudioServicesPlaySystemSound(soundID);
}
}
I've seen answers on how this is made using numberOfRepeats. But I can't fit it in my code.
Try using AVAudioPlayer:
- (void) playSound:(CFURLRef)soundFileURLRef {
NSError* sndErr;
AVAudioPlayer *player = [[ AVAudioPlayer alloc ] initWithContentsOfURL:(NSURL *)soundFileURLRef error:(&sndErr) ];
// at this point player has a retain count of 1. you should save it
// somewhere like a class member variable so you can stop the playing
// and release the object later. Under ARC, you must have a strong reference
// to the object so that it doesn't get released when this function goes out
// of scope.
if (sndErr != nil) {
player.numberOfLoops = -1; // infinite number of loops. call stop when you want to stop.
[player play];
}
}