n65 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/Gemfile +4 -0
- data/LICENSE +340 -0
- data/README.md +126 -0
- data/Rakefile +2 -0
- data/bin/n65 +11 -0
- data/data/opcodes.yaml +1030 -0
- data/examples/beep.asm +24 -0
- data/examples/mario2.asm +260 -0
- data/examples/mario2.char +0 -0
- data/examples/music_driver.asm +202 -0
- data/examples/noise.asm +93 -0
- data/examples/pulse_chord.asm +213 -0
- data/images/assembler_demo.png +0 -0
- data/lib/n65.rb +243 -0
- data/lib/n65/directives/ascii.rb +42 -0
- data/lib/n65/directives/bytes.rb +102 -0
- data/lib/n65/directives/dw.rb +86 -0
- data/lib/n65/directives/enter_scope.rb +55 -0
- data/lib/n65/directives/exit_scope.rb +35 -0
- data/lib/n65/directives/inc.rb +67 -0
- data/lib/n65/directives/incbin.rb +51 -0
- data/lib/n65/directives/ines_header.rb +53 -0
- data/lib/n65/directives/label.rb +46 -0
- data/lib/n65/directives/org.rb +47 -0
- data/lib/n65/directives/segment.rb +45 -0
- data/lib/n65/directives/space.rb +46 -0
- data/lib/n65/front_end.rb +90 -0
- data/lib/n65/instruction.rb +308 -0
- data/lib/n65/instruction_base.rb +29 -0
- data/lib/n65/memory_space.rb +150 -0
- data/lib/n65/opcodes.rb +9 -0
- data/lib/n65/parser.rb +85 -0
- data/lib/n65/regexes.rb +33 -0
- data/lib/n65/symbol_table.rb +198 -0
- data/lib/n65/version.rb +3 -0
- data/n65.gemspec +23 -0
- data/nes_lib/nes.sym +105 -0
- data/test/test_memory_space.rb +82 -0
- data/test/test_symbol_table.rb +238 -0
- data/utils/midi/Makefile +3 -0
- data/utils/midi/c_scale.mid +0 -0
- data/utils/midi/convert +0 -0
- data/utils/midi/guitar.mid +0 -0
- data/utils/midi/include/event.h +93 -0
- data/utils/midi/include/file.h +57 -0
- data/utils/midi/include/helpers.h +14 -0
- data/utils/midi/include/track.h +45 -0
- data/utils/midi/lil_melody.mid +0 -0
- data/utils/midi/mi_feabhra.mid +0 -0
- data/utils/midi/midi_to_nes.rb +204 -0
- data/utils/midi/source/convert.cpp +16 -0
- data/utils/midi/source/event.cpp +96 -0
- data/utils/midi/source/file.cpp +37 -0
- data/utils/midi/source/helpers.cpp +46 -0
- data/utils/midi/source/track.cpp +37 -0
- data/utils/opcode_table_to_yaml.rb +91 -0
- metadata +133 -0
data/utils/midi/Makefile
ADDED
Binary file
|
data/utils/midi/convert
ADDED
Binary file
|
Binary file
|
@@ -0,0 +1,93 @@
|
|
1
|
+
#ifndef MIDI_EVENT_H
|
2
|
+
#define MIDI_EVENT_H
|
3
|
+
|
4
|
+
#include <stdio.h>
|
5
|
+
#include <stdlib.h>
|
6
|
+
#include "helpers.h"
|
7
|
+
|
8
|
+
namespace Midi {
|
9
|
+
|
10
|
+
enum MidiMessageType {Midi = 0, MidiNoteOff = 0x8, MidiNoteOn, MidiPolyAftertouch,
|
11
|
+
MidiControlChange, MidiProgramChange, MidiChannelAftertouch,
|
12
|
+
MidiPitchWheel, MidiMetaEvent = 0xFF, MidiSysex1 = 0xF0, MidiSysex2 = 0xF7};
|
13
|
+
|
14
|
+
|
15
|
+
enum MidiMetaType {MetaSequenceNumber = 0x0, MetaTextEvent = 0x1, MetaCopyright = 0x2,
|
16
|
+
MetaTrackName = 0x3, MetaInstrumentName = 0x4, MetaLyricText = 0x5,
|
17
|
+
MetaMarkerText = 0x6, MetaCuePoint = 0x7, MetaChannelPrefixAssignment = 0x20,
|
18
|
+
MetaEndOfTrack = 0x2F, MetaTempoSetting = 0x51, MetaSMPTEOffset = 0x54,
|
19
|
+
MetaTimeSignature = 0x58, MetaKeySignature = 0x59, MetaSequenceSpecific = 0x7F};
|
20
|
+
|
21
|
+
/***
|
22
|
+
* Spec:
|
23
|
+
* MidiTrackEvent = <delta_time (Variable)> + <MidiEvent> | <MetaEvent> | <SysexEvent>
|
24
|
+
* MidiEvent = <MidiMessageType (4 bits)> + <channel (4 bits)> + <parameter1 (1 byte)> <parameter2 (1 byte)>
|
25
|
+
* MetaEvent = 0xFF + <MidiMetaType> + <size (Variable)> + <data>
|
26
|
+
* SysexEvent = 0xF0 + <data> + 0xF7 -or-
|
27
|
+
* SysexEvent = 0xF7 + <data> 0xF7
|
28
|
+
***/
|
29
|
+
|
30
|
+
class Event {
|
31
|
+
private:
|
32
|
+
unsigned int m_delta;
|
33
|
+
unsigned int m_size;
|
34
|
+
unsigned int m_data_size;
|
35
|
+
unsigned char m_status;
|
36
|
+
unsigned char m_meta_type;
|
37
|
+
unsigned char m_parameter1;
|
38
|
+
unsigned char m_parameter2;
|
39
|
+
void *m_data;
|
40
|
+
|
41
|
+
public:
|
42
|
+
Event();
|
43
|
+
Event(int delta, int status, int parameter1, int parameter2);
|
44
|
+
void init_midi(int delta, int status, int parameter1, int parameter2);
|
45
|
+
void init_from_file(FILE *fp, unsigned char last_status);
|
46
|
+
~Event();
|
47
|
+
|
48
|
+
// Some easy inline functions
|
49
|
+
static bool is_status_byte(unsigned char byte) { return ((byte & 0x80) >> 7) == 1; }
|
50
|
+
unsigned int delta() const { return m_delta; }
|
51
|
+
unsigned char status() const { return m_status; }
|
52
|
+
unsigned int bytes_read() const { return m_size; }
|
53
|
+
unsigned char parameter1() const { return m_parameter1; }
|
54
|
+
unsigned char parameter2() const { return m_parameter2; }
|
55
|
+
int message_type() const { return (m_status &0xF0) >> 4; }
|
56
|
+
int channel() const { return (m_status &0xF); }
|
57
|
+
void *sysex_data() const { return m_data; }
|
58
|
+
|
59
|
+
|
60
|
+
void print_yaml(){
|
61
|
+
printf(" - :delta: 0x%X\n", m_delta);
|
62
|
+
printf(" :status: 0x%X\n", m_status);
|
63
|
+
|
64
|
+
switch(m_status){
|
65
|
+
case 0xFF:
|
66
|
+
printf(" :meta_type: 0x%X\n", m_meta_type);
|
67
|
+
printf(" :meta_data_size: 0x%X\n", m_data_size);
|
68
|
+
break;
|
69
|
+
case 0xF0:
|
70
|
+
case 0xF7:
|
71
|
+
printf(" :sysex_data_size: %d\n", m_data_size);
|
72
|
+
break;
|
73
|
+
default:
|
74
|
+
printf(" :parameter1: 0x%X\n", m_parameter1);
|
75
|
+
printf(" :parameter2: 0x%X\n", m_parameter2);
|
76
|
+
}
|
77
|
+
/* Printing out the actual data as a string confuses the YAML parser
|
78
|
+
* So let's just not do that.
|
79
|
+
if(m_data){
|
80
|
+
printf(" :data: \"");
|
81
|
+
|
82
|
+
for(int i = 0; i < m_data_size; i++){
|
83
|
+
printf("%c", ((unsigned char*)m_data)[i]);
|
84
|
+
}
|
85
|
+
printf("\"\n");
|
86
|
+
}
|
87
|
+
*/
|
88
|
+
}
|
89
|
+
};
|
90
|
+
|
91
|
+
}
|
92
|
+
|
93
|
+
#endif
|
@@ -0,0 +1,57 @@
|
|
1
|
+
#ifndef MIDI_FILE_H
|
2
|
+
#define MIDI_FILE_H
|
3
|
+
|
4
|
+
#include <vector>
|
5
|
+
#include "track.h"
|
6
|
+
|
7
|
+
namespace Midi {
|
8
|
+
|
9
|
+
/***
|
10
|
+
* Spec:
|
11
|
+
* MidiFile = <midi_header_t> + <MidiTrack> [+ <MidiTrack> ...]
|
12
|
+
*
|
13
|
+
* midi_header_t = "MThd" + <size (4 bytes)> + <format (2 bytes)> + <track_count (2 bytes)> + <ticks_per_quarter_note (2 bytes)>
|
14
|
+
*
|
15
|
+
***/
|
16
|
+
|
17
|
+
typedef struct midi_header_t {
|
18
|
+
char cookie[4];
|
19
|
+
unsigned int size;
|
20
|
+
unsigned short format;
|
21
|
+
unsigned short track_count;
|
22
|
+
unsigned short ticks_per_quarter_note;
|
23
|
+
} __attribute__((packed)) midi_header_t;
|
24
|
+
|
25
|
+
|
26
|
+
class File {
|
27
|
+
private:
|
28
|
+
midi_header_t m_header;
|
29
|
+
|
30
|
+
public:
|
31
|
+
File(void);
|
32
|
+
~File(void);
|
33
|
+
std::vector<Track *> m_tracks;
|
34
|
+
void init_from_file(const char *filename);
|
35
|
+
|
36
|
+
void print_yaml(){
|
37
|
+
printf("---\n");
|
38
|
+
printf(":midi_file:\n");
|
39
|
+
printf(" :header: Mthd\n");
|
40
|
+
printf(" :size: %u\n", m_header.size);
|
41
|
+
printf(" :format: %u\n", m_header.format);
|
42
|
+
printf(" :track_count: %u\n", m_header.track_count);
|
43
|
+
printf(" :ticks_per_quarter_note: %u\n", m_header.ticks_per_quarter_note);
|
44
|
+
printf(" :tracks:\n");
|
45
|
+
|
46
|
+
for(int i = 0; i < m_tracks.size(); i++){
|
47
|
+
if(i == 0){
|
48
|
+
m_tracks[i]->print_yaml();
|
49
|
+
}else{
|
50
|
+
m_tracks[i]->print_yaml();
|
51
|
+
}
|
52
|
+
}
|
53
|
+
}
|
54
|
+
};
|
55
|
+
|
56
|
+
}
|
57
|
+
#endif
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#ifndef MIDI_HELPERS_H
|
2
|
+
#define MIDI_HELPERS_H
|
3
|
+
|
4
|
+
#include <stdio.h>
|
5
|
+
#include <stdlib.h>
|
6
|
+
|
7
|
+
namespace Midi {
|
8
|
+
|
9
|
+
unsigned int read_variable_length(FILE *fp, unsigned int *value_size);
|
10
|
+
short swap_endian_16(short big_endian);
|
11
|
+
int swap_endian_32(int big_endian);
|
12
|
+
|
13
|
+
}
|
14
|
+
#endif
|
@@ -0,0 +1,45 @@
|
|
1
|
+
#ifndef MIDI_TRACK_H
|
2
|
+
#define MIDI_TRACK_H
|
3
|
+
|
4
|
+
#include "event.h"
|
5
|
+
#include <vector>
|
6
|
+
|
7
|
+
namespace Midi {
|
8
|
+
|
9
|
+
/***
|
10
|
+
* Spec:
|
11
|
+
* MidiTrack = <midi_track_header_t> + <MidiEvent> [+ <MidiEvent> ...]
|
12
|
+
* midi_track_header_t = "MTrk" + <size (4 bytes)>
|
13
|
+
*
|
14
|
+
***/
|
15
|
+
|
16
|
+
typedef struct midi_track_header_t {
|
17
|
+
char cookie[4];
|
18
|
+
unsigned int size;
|
19
|
+
} __attribute__((packed)) midi_track_header_t;
|
20
|
+
|
21
|
+
|
22
|
+
class Track {
|
23
|
+
private:
|
24
|
+
unsigned int m_total_size;
|
25
|
+
midi_track_header_t m_header;
|
26
|
+
|
27
|
+
public:
|
28
|
+
Track(void);
|
29
|
+
~Track(void);
|
30
|
+
void init_from_file(FILE *fp);
|
31
|
+
std::vector<Event *> m_events;
|
32
|
+
|
33
|
+
void print_yaml(){
|
34
|
+
printf(" - :header: MTrk\n");
|
35
|
+
printf(" :total_size: %u\n", m_header.size);
|
36
|
+
printf(" :events:\n");
|
37
|
+
|
38
|
+
for(int i = 0; i < m_events.size(); i++){
|
39
|
+
m_events[i]->print_yaml();
|
40
|
+
}
|
41
|
+
}
|
42
|
+
};
|
43
|
+
|
44
|
+
}
|
45
|
+
#endif
|
Binary file
|
Binary file
|
@@ -0,0 +1,204 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
class MidiToNES
|
6
|
+
|
7
|
+
#### Custom Exceptions
|
8
|
+
class MidiFormatNotSupported < StandardError; end
|
9
|
+
|
10
|
+
#### Some Constants
|
11
|
+
NoteOff = 0x8
|
12
|
+
NoteOn = 0x9
|
13
|
+
|
14
|
+
#### A440 Tuning, and NES CPU speed in hz
|
15
|
+
Tuning = 440.0
|
16
|
+
CPU = 1789773.0
|
17
|
+
|
18
|
+
|
19
|
+
#### LSB Address registers of the APU, MSB is always 0x40
|
20
|
+
Pulse1Control = 0x00
|
21
|
+
Pulse1FT = 0x2
|
22
|
+
Pulse1CT = 0x3
|
23
|
+
|
24
|
+
|
25
|
+
####
|
26
|
+
## Initialize from a yaml file
|
27
|
+
def self.init_from_file(filename, bpm)
|
28
|
+
self.new(File.read(filename), bpm)
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
####
|
33
|
+
## Initialize with a yaml string
|
34
|
+
def initialize(yaml_string, bpm)
|
35
|
+
@bpm = bpm.to_f
|
36
|
+
@midi_data = YAML.load(yaml_string)[:midi_file]
|
37
|
+
unless @midi_data[:format].zero?
|
38
|
+
fail(MidiFormatNotSupported, "Currently only supports format 0 Midi Files")
|
39
|
+
end
|
40
|
+
@ticks_per_quarter_note = @midi_data[:ticks_per_quarter_note]
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
####
|
45
|
+
## Write to binary file
|
46
|
+
def write_binary(filename)
|
47
|
+
binary = convert
|
48
|
+
File.open(filename, 'wb') do |fp|
|
49
|
+
fp.write(binary)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
####
|
55
|
+
## For now assume one track
|
56
|
+
def convert
|
57
|
+
tick_count = 1
|
58
|
+
events = []
|
59
|
+
|
60
|
+
track = @midi_data[:tracks].first
|
61
|
+
track[:events].each do |event|
|
62
|
+
|
63
|
+
delta, status, note, velocity = event.values_at(:delta, :status, :parameter1, :parameter2)
|
64
|
+
|
65
|
+
## The status byte contains both the Midi message type, and channel.
|
66
|
+
type = (status & 0b11110000) >> 4
|
67
|
+
channel = status & 0b00001111
|
68
|
+
|
69
|
+
## We only care about note on and off, and only care about channel 0 for now.
|
70
|
+
next unless type == NoteOn || type == NoteOff
|
71
|
+
next unless channel.zero?
|
72
|
+
|
73
|
+
## Update the total time
|
74
|
+
tick_count += delta
|
75
|
+
|
76
|
+
## Ok this is a note either turning on or off
|
77
|
+
if type == NoteOff || velocity.zero?
|
78
|
+
#event = {:start => tick_count, :note => note, :velocity => 0}
|
79
|
+
#events << event
|
80
|
+
else
|
81
|
+
event = {:start => tick_count, :note => note, :velocity => velocity}
|
82
|
+
events << event
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
## Finally sort event list by start time
|
87
|
+
events.sort! do |a, b|
|
88
|
+
a[:start] <=> b[:start]
|
89
|
+
end
|
90
|
+
|
91
|
+
## Now convert these events to a bytestream for our NES sound engine
|
92
|
+
events_to_byte_stream(events)
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
####
|
97
|
+
## This converts a list of note events into a byte stream for updating NES APU registers
|
98
|
+
def events_to_byte_stream(events)
|
99
|
+
last_tick = 1
|
100
|
+
byte_stream = []
|
101
|
+
|
102
|
+
events.each do |event|
|
103
|
+
## Work out the delta again
|
104
|
+
delta = event[:start] - last_tick
|
105
|
+
byte_stream << midi_tick_to_vblank(delta) # Delta
|
106
|
+
byte_stream << pulse_control_value(event) # Value
|
107
|
+
if event[:velocity].zero?
|
108
|
+
#byte_stream << 0 # Off with 0 frequency timer
|
109
|
+
#byte_stream << 0
|
110
|
+
else
|
111
|
+
byte_stream << pulse_ft_value(event) # Value
|
112
|
+
byte_stream << pulse_ct_value(event) # Value
|
113
|
+
end
|
114
|
+
last_tick += delta
|
115
|
+
end
|
116
|
+
byte_stream.pack('C*')
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
####
|
121
|
+
## Given an event, produce a value for register nes.apu.pulse1.control
|
122
|
+
## DDLC VVVV
|
123
|
+
## Duty (D), envelope loop / length counter halt (L), constant volume (C), volume/envelope (V)
|
124
|
+
def pulse_control_value(event)
|
125
|
+
## Start with 50% duty cycle, length counter halt is on
|
126
|
+
## Constant volume is On, and volume is determined by bit-reducing the event velocity to 4-bit
|
127
|
+
value = 0b10000111
|
128
|
+
|
129
|
+
#four_bit_max = (2**4 - 1)
|
130
|
+
#seven_bit_max = (2**7 - 1)
|
131
|
+
|
132
|
+
#volume_float = event[:velocity] / seven_bit_max.to_f
|
133
|
+
#volume_4_bit = (volume_float * four_bit_max).round & 0b00001111
|
134
|
+
|
135
|
+
#value | volume_4_bit
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
####
|
140
|
+
## Given an event, produce a value for register nes.apu.pulse1.ft
|
141
|
+
## TTTT TTTT
|
142
|
+
## This is the low byte of the timer, the higher few bits being in pulse1.ct
|
143
|
+
def pulse_ft_value(event)
|
144
|
+
midi_note_to_nes_timer(event[:note]) & 0xff
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
####
|
149
|
+
## Given an event, produce a value for register nes.apu.pulse1.ct
|
150
|
+
## LLLL LTTT
|
151
|
+
## This has the higher 3 bits of the timer, and L is the length counter.
|
152
|
+
## For now let's just use duration as the length counter.
|
153
|
+
def pulse_ct_value(event)
|
154
|
+
value = 0b11111000
|
155
|
+
|
156
|
+
## We will grab the high 3 bits of the 11-bit timer value now
|
157
|
+
timer_high_3bit = midi_note_to_nes_timer(event[:note]) & 0b11100000000
|
158
|
+
value | (timer_high_3bit >> 8)
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
####
|
163
|
+
## Midi note to NES timer
|
164
|
+
def midi_note_to_nes_timer(midi_note)
|
165
|
+
frequency = Tuning * 2**((midi_note - 69) / 12.0)
|
166
|
+
timer = (CPU / (16 * frequency)) - 1
|
167
|
+
if timer > (2**11 - 1)
|
168
|
+
fail("midi note #{midi_note} is too big at #{timer}")
|
169
|
+
end
|
170
|
+
timer.round
|
171
|
+
end
|
172
|
+
|
173
|
+
|
174
|
+
####
|
175
|
+
## Convert a MIDI tick delta to an NES vblank delta.
|
176
|
+
def midi_tick_to_vblank(midi_tick)
|
177
|
+
quarter_note_in_seconds = 60 / @bpm
|
178
|
+
vblanks_per_quarter_note = quarter_note_in_seconds / (1/60.0)
|
179
|
+
tick_normalized = midi_tick / @ticks_per_quarter_note.to_f
|
180
|
+
vblanks = tick_normalized * vblanks_per_quarter_note
|
181
|
+
vblanks.round
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
|
186
|
+
if __FILE__ == $0
|
187
|
+
unless ARGV.size == 2
|
188
|
+
STDERR.puts("Usage #{$0} <bpm> <music.mid>")
|
189
|
+
exit(1)
|
190
|
+
end
|
191
|
+
|
192
|
+
bpm, midi_file = ARGV
|
193
|
+
|
194
|
+
## Run the midi file through my converter written in C++
|
195
|
+
IO.popen("./convert #{midi_file}") do |io|
|
196
|
+
midi_to_nes = MidiToNES.new(io.read, bpm.to_i)
|
197
|
+
midi_to_nes.write_binary('../../data.mus')
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
|
204
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
#include <stdio.h>
|
2
|
+
#include "file.h"
|
3
|
+
|
4
|
+
int main(int argc, char **argv){
|
5
|
+
|
6
|
+
if(argc < 1){
|
7
|
+
printf("Need a midi file argument\n");
|
8
|
+
exit(1);
|
9
|
+
}
|
10
|
+
|
11
|
+
Midi::File midi_file;
|
12
|
+
midi_file.init_from_file(argv[1]);
|
13
|
+
midi_file.print_yaml();
|
14
|
+
|
15
|
+
return 0;
|
16
|
+
}
|