I'm trying to create a music game using godot, it's similar to playing a piano, the player presses keys, each key has a sound assigned, and the music plays. What I want is to have a kind of recorder inside the game to be able to save the melody created and export it to some file like mp3 or wav. I'm using gdScript and I've been researching about this but wasn't able to find anything.
This issue has two parts, first of all, we need to actually reproduce the audio, and second we need to record it and save it a file. And we have two options for playing the audio... Let us begin there.
Playing
Playing audio samples
As you probably know you can use AudioStreamPlayer(2D/3D) to play audio. If you have samples an instrument, you can set up multiple of these, one for each note. And then play them individually by calling play.
Alright, but what if you don't have a sample for each note? Just one.
Well, you can use pitch_scale on your AudioStreamPlayer(2D/3D), but that also shortens the note. Instead…
Look at the bottom of the Godot editor, there is a panel called Audio. There you can configure audio buses. Add a new bus, add the PitchShift effect to it. If you select the effect, you can configure it on the Inspector panel. Now, in your AudioStreamPlayer(2D/3D), you can select the bus that has the effect, and there you go.
Except, you would have to setup a lot of these. So, let us do that from code instead.
First, adding an AudioStreamPlayer(2D/3D), is adding a node:
var player := AudioStreamPlayer.new()
add_child(player)
We want to set the sample, so we also need to load the sample:
var sample := preload("res://sample.ogg")
var player := AudioStreamPlayer.new()
player.stream = sample # <--
add_child(player)
By the way, double check in the import settings of your sample (With the file selected in the FileSystem panel, go to the Import panel) if it has loop enabled or not.
To set the audio bus, you can do this:
var sample := preload("res://sample.ogg")
var player := AudioStreamPlayer.new()
player.stream = sample
player.bus = "Bus Name" # <--
add_child(player)
And that brings me to adding an audio bus:
AudioServer.add_bus()
Wait, hmm… Ok, let us put the bus explicitly at the end, so we know the index:
var bus_index := AudioServer.get_bus_count()
AudioServer.add_bus(bus_index)
And give it a name:
var bus_index := AudioServer.get_bus_count()
AudioServer.add_bus(bus_index)
AudioServer.set_bus_name(bus_index, "Bus Name") # <--
And let us route it to Master:
var bus_index := AudioServer.get_bus_count()
AudioServer.add_bus(bus_index)
AudioServer.set_bus_name(bus_index, "Bus Name")
AudioServer.set_bus_send(bus_index, "Master") # <--
Yes, it is set to Master by default. But you might need to change that.
And now we can add the effect:
var pitch_shift_effect := AudioEffectPitchShift.new()
pitch_shift_effect.pitch_scale = 0.1
AudioServer.add_bus_effect(bus_index, pitch_shift_effect, 0)
That 0 at the end is the index of the effect. Since this is the only effect in a newly created audio bus, it goes in the index 0.
And, well, you can make a loop and add the players with their corresponding audio buses with their pitch shift effects.
Generating audio
Alright, perhaps you don't have a sample at all. Instead you want to generate waves. We can do that too.
This time we are going to have a AudioStreamPlayer(2D/3D), and give it an AudioStreamGenerator (you find it in the drop down menu from the stream property in the Inspecto panel). Edit the resource to set the mix rate you want to work with, by default it is 44100. But we are going to be generating this audio as close as real time as possible, so a lower rate might be necesary.
Now, in a script, we are going to get the playback object form the AudioStreamPlayer(2D/3D):
onready var _playback := $Player.get_stream_playback()
Or if you are placing the script in the player, because why not:
onready var _playback := get_stream_playback()
We can push audio frames to the playback object. With playback.push_frame, it takes a Vector2, where each component is one of the stereo channels (x = left, y = right).
We are going to call get_frames_available to figure out how many we need to push, and we are going to be pushing them every graphics frame (i.e. in _process).
The following script will generate an sine wave with frequency 440 (A):
extends AudioStreamPlayer
onready var _playback := get_stream_playback()
onready var _sample_hz:float = stream.mix_rate
var _pulse_hz := 440.0
var _phase := 0.0
func _ready():
_fill_buffer()
func _process(_delta):
_fill_buffer()
func _fill_buffer():
var increment := _pulse_hz / _sample_hz
for frame_index in int(_playback.get_frames_available()):
_playback.push_frame(Vector2.ONE * sin(_phase * TAU))
_phase = fmod(_phase + increment, 1.0)
You can set the AudioStreamPlayer to autoplay.
This code is adapted from the official Audio Generator demo.
We, of course, may want to play multiple of these notes at the same time.
So, I created a Note class that looks like this (just add a new script in the FileSystem panel):
class_name Note extends Object
var increment:float
var pulse_hz:float
var phase:float
func _init(hz:float, sample_hz):
pulse_hz = hz
phase = 0.0
increment = pulse_hz / sample_hz
func frame() -> float:
var result := sin(phase * TAU)
phase = fmod(phase + increment, 1.0)
return result
And now we can play them like this:
extends AudioStreamPlayer
onready var _playback := get_stream_playback()
onready var _sample_hz:float = stream.mix_rate
onready var notes := [
Note.new(440, _sample_hz),
Note.new(554.37, _sample_hz),
Note.new(622.25, _sample_hz)
]
func _ready():
_fill_buffer()
func _process(_delta):
_fill_buffer()
func _fill_buffer():
var note_count := notes.size()
for frame_index in int(_playback.get_frames_available()):
var frame := 0.0
for note in notes:
frame += note.frame()
_playback.push_frame(Vector2.ONE * frame / note_count)
Recording
To record we are going to add a "Record" (AudioEffectRecord) effect to an audio bus. Let us say you added it to Master (the first bus), and it is the first effect there, we can get if from code like this:
var record_bus_index := 0 # Master
var record_effect_index := 0 # First Effect
var record_effect = AudioServer.get_bus_effect(record_bus_index, record_effect_index) as AudioEffectRecord
Then we need to start recording, like this:
record_effect.set_recording_active(true)
And when we are done recording we can get what was recorded (an AudioStreamSample) and stop recording:
var recording := record_effect.get_recording()
record_effect.set_recording_active(false)
And finally we can save it to a WAV file:
recording.save_to_wav("user://recording.wav")
See also the official Audio Mic Record demo project.
No, there is no in engine solution to save an MP3.
Linky links
The official Audio Generator demo.
The official Audio Mic Record demo project.
The addon godot-simple-sampler.
The addon godot-midi-player.
This is a jwplayer case. I have a website with music album pages. Right now I have a jwplayer playing a playlist and works fine, but I want to have a table listing all the songs by track number and a play icon that plays the song on the first of each line. So far I managed to create a play icon that plays the full playlist but no luck on how to create as many icons as the number of songs and that each plays a different song from the playlist.
This is a representation of what I want to do
play icon (song 1) | track number | title song 1 | duration
play icon (song 2) | track number | title song 2 | duration
The current code I have is this, which creates a single play icon that plays the playlist. This code, by the way is pretty awesome as it plays next song automatically and controls the pause/play of the main player. This website runs on joomla.
<script>
var playerInstance = jwplayer('container');
playerInstance.setup({
playlist: [{
//DYNAMICALLY GENERATED PLAYLIST
<?php
$album_id = $this->item->id;
// Get default database object
$db = JFactory::getDbo();
// Get a new JDatabaseQuery object
$query = $db->getQuery(true);
$query = 'SELECT position, name, length, filename FROM XXX WHERE `album_id` = ' . $db->quote( (int) $album_id ) . ' ORDER BY `num` ASC LIMIT 0 , 30';
$db->setQuery($query);
$results = $db -> loadAssocList();
foreach($results as $row){
echo "file: \"/". htmlentities($row['filename']). "\", ";
echo "title: \"". htmlentities($row['name']). "\"";
echo "},{";
}
?> }]
//END DYNAMICALLY GENERATED PLAYLIST
controls: false
});
playerInstance.on('play', function() {
document.getElementById("videoPlayPause").style.backgroundPosition = "-263px -70px";
});
playerInstance.on('pause', function() {
document.getElementById("videoPlayPause").style.backgroundPosition = "-287px -70px";
});
</script>
<br/>
<div class="videoPlayerControls playing" id="videoPlayerControls">
<button id="videoPlayPause" onclick="playerInstance.play();" onmouseout="if(playerInstance.getState() == "IDLE" || playerInstance.getState() == "PAUSED"){document.getElementById("videoPlayPause").style.backgroundPosition = "-62px 0";} if(playerInstance.getState() == "PLAYING"){document.getElementById("videoPlayPause").style.backgroundPosition = "-62px -50px";}" onmouseover="if(playerInstance.getState() == "PLAYING"){document.getElementById("videoPlayPause").style.backgroundPosition = "-263px -70px";}if(playerInstance.getState() == "IDLE" || playerInstance.getState() == "PAUSED"){document.getElementById("videoPlayPause").style.backgroundPosition = "-287px -70px";}" type="button">Play/Pause</button>
Anyone has done something like this?
In JavaScript, you just need to load the URL of the file you want to play into the player, like this.
<script>
jwplayer('yourPlayerDivId').setup({
file: 'track1.mp4'
});
</script>
Given you have a loaded playlist, you can access (play) any item in the playlist directly using the playerInstance.playlistItem(index) method.
This starts playback of the playlist item at the specified index - bearing in mind that the first item in the playlist would have an index of 0.
You should therefore be able to attach a click event to each corresponding 'Play Icon' which calls this method with an incremented index in order to access the associated playlist item.
BTW: Since JW7 the playerInstance.getState() reports the state as a lowercase string...
I'm working on a simple mp3 player application in Flash and am trying to get a time counter implemented that will keep track of how much time has elapsed from the currently playing track. I came across this code snippet online:
var music:Sound = new Sound();
music.loadSound("audio.mp3", true);
var minutes:Number = 0;
var seconds:Number = 0;
this.onEnterFrame = function() {
minutes = Math.floor(music.position / 1000 / 60);
seconds = Math.floor(music.position / 1000) % 60;
output.text = minutes + ":" + seconds;
}
However it seems that the position property is no longer part of the Sound class. I'm new to ActionScript, has this property been moved elsewhere? Or does someone have any thoughts on how to implement a time counter for mp3 progress?
The play method in Sound returns a SoundChannel which has the position property you're looking for.
The current state
As from my link you can pick different regions on the map and everything seems to be working until you re-select a county you have selected before. Data values stored with each path decide if it isSelected or notSelected. I have no problem in changing the element data just clicked with this but I can't find a way of storing the last element selected in a way that I can change it's element data. Which means I first have to click on the previous county to set it's element data to notSelected
First I define var currentcountyselected = "";. This allows me to store the paths[arr[this.id]].name;. When I click on a new path I can make the last path fill change with $('#'+currentcountyselected).attr({fill: attributes.fill});
In Raphael's for loop I set obj.data('selected', 'notSelected'); so all path elements are set to notSeelected.
So what I need is some way to store the last path so I can change it's element data
This is the click function cleaned up from live example.
obj.click(function(){
if(this.data('selected') == 'notSelected')
{this.animate({fill: '#698B22' }, 300);
this.data('selected', 'isSelected');
$('#'+currentcountyselected).attr({fill: attributes.fill});
paths[arr[this.id]].value = "isSelected";
currentcountyselected = paths[arr[this.id]].name;
}
else
{this.animate({fill: '#32CD32'}, 300);
paths[arr[this.id]].value = "notSelected"; /* set path value*/
this.data('selected', 'notSelected');
}
});/* end mark selections */
I've been working on this project for a while and the client now wants the interface to work differently. This has really ate up my hourse.
EDIT:Although I have found a solution by simply taking out the if/else I would still like to know how to get at element data in a previous path (or any path for that matter).
Here is my solution, posted as it might help someone. The link in my question has problems with click happy users.
Globals
var previouscountyselected = "Mayo"; /* default start, can be any county(path) */
var start = true;
var past = null;
Changed code
obj.click(function(){
if(paths[arr[this.id]].value == 'notSelected')
{
this.animate({fill: '#698B22'}, 200);
paths[previouscountyselected].value = "notSelected";
paths[arr[this.id]].value = "isSelected";
previouscountyselected = paths[arr[this.id]].name;
if (!start && past != this)
{
past.animate({ fill: '#fff' }, 200);
}
past = this;
start = false;
}
else if(paths[arr[this.id]].value == 'isSelected')
{
this.animate({fill: '#32CD32'}, 200);
paths[arr[this.id]].value = "notSelected"; /* set path value */
}
});
Overview
if (!start && past != this) is a little unusual and is required or animated fades get messed up and choppy. The fade is not triggered if it is the first time a path is clicked and if you just hammer clicks on one path it doesn't fade to white. The main if/else handles the actual control value.
Until I get a jsfiddle up this link will demonstrate the desired behaviour.
Note! the drop menu in this link does not work.
Click happy friendly
I have buttons with id's button1, button2, button3, etc.
I have a for loop that i need to loop starting with a number and i need to enable buttons based on my loop. I need help taking my string button1 and making that the id "button1" so i can use the button property's.
Any help would be greatly appreciated.
Thanks!
You should be able to iterate as such:
for (var i:uint = 1; i <= 3; i++)
{
var element:IVisualElement = this["group" + i];
}
You don't denote whether this Spark or Halo, but clearly substitute IVisualElement with the type of your choice. (Button, UIComponent, DisplayObject...)