File Coverage

blib/lib/Audio/SID.pm
Criterion Covered Total %
statement 18 510 3.5
branch 0 280 0.0
condition 0 168 0.0
subroutine 6 40 15.0
pod 34 34 100.0
total 58 1032 5.6


line stmt bran cond sub pod time code
1             package Audio::SID;
2              
3             require 5;
4              
5 1     1   8301 use Carp;
  1         2  
  1         84  
6 1     1   5 use strict;
  1         2  
  1         53  
7 1     1   5 use vars qw($VERSION);
  1         6  
  1         49  
8 1     1   1134 use FileHandle;
  1         14826  
  1         7  
9 1     1   384 use Digest::MD5;
  1         2  
  1         36  
10 1     1   993 use Encode;
  1         11625  
  1         7049  
11              
12             $VERSION = "3.11";
13              
14             # These are the recognized field names for a SID file. They must appear in
15             # the order they appear in a SID file.
16             my (@SIDfieldNames) = qw(magicID version dataOffset loadAddress initAddress
17             playAddress songs startSong speed title author
18             released flags startPage pageLength reserved data);
19              
20             # Additional data stored in the class that are not part of the SID file
21             # format are: FILESIZE, FILENAME, and the implicit REAL_LOAD_ADDRESS.
22             #
23             # PADDING is used to hold any extra bytes that may be between the standard
24             # SID header and the data (usually happens when dataOffset is more than
25             # 0x007C).
26              
27             # Constants for individual fields inside 'flags'.
28             my $MUSPLAYER_OFFSET = 0; # Bit 0.
29             my $PLAYSID_OFFSET = 1; # Bit 1. (PSID v2NG only)
30             my $C64BASIC_OFFSET = 1; # Bit 1. (RSID only)
31             my $CLOCK_OFFSET = 2; # Bits 2-3.
32             my $SIDMODEL_OFFSET = 4; # Bits 4-5.
33              
34             sub new {
35 0     0 1   my $type = shift;
36 0           my %params = @_;
37 0   0       my $class = ref($type) || $type;
38 0           my $self = {};
39              
40 0           bless ($self, $class);
41              
42 0           $self->initialize();
43              
44 0           $self->{validateWrite} = 0;
45              
46 0 0         if (defined($_[0])) {
47             # Read errors are taken care of by read().
48 0 0         return ($self->read(%params) ? $self : undef);
49             }
50              
51 0           return $self;
52             }
53              
54             sub initialize() {
55 0     0 1   my ($self) = $_[0];
56              
57             # Initial SID data.
58 0           $self->{SIDdata} = {
59             magicID => 'PSID',
60             version => 2,
61             dataOffset => 0x7C,
62             loadAddress => 0,
63             initAddress => 0,
64             playAddress => 0,
65             songs => 1,
66             startSong => 1,
67             speed => 0,
68             title => '',
69             author => '',
70             released => '20?? ',
71             flags => 0,
72             startPage => 0,
73             pageLength => 0,
74             reserved => 0,
75             data => '',
76             };
77              
78 0           $self->{PADDING} = '';
79              
80 0           $self->{FILESIZE} = 0x7C;
81 0           $self->{FILENAME} = '';
82             }
83              
84             sub read {
85 0     0 1   my $self = shift;
86 0           my $filename;
87             my $filedata;
88 0           my $hdr;
89 0           my $i;
90 0           my ($size, $totsize);
91 0           my $data;
92 0           my $FH;
93 0           my ($SID, $version, $dataOffset);
94 0           my @hdr;
95 0           my $hdrlength;
96              
97             # Check parameters.
98              
99 0 0 0       if (($_[0] =~ /^\-filedata$/i) and defined($_[1])) {
    0 0        
    0 0        
    0          
100 0           $filedata = $_[1];
101             }
102             elsif (($_[0] =~ /^\-file(name)|(handle)$/i) and defined($_[1])) {
103 0           $filename = $_[1];
104             }
105             elsif (defined($_[0]) and !defined($_[1])) {
106 0           $filename = $_[0];
107             }
108             elsif (defined($_[0])) {
109 0           confess("Unknown parameter '$_[0]'!");
110 0           $self->initialize();
111 0           return undef;
112             }
113              
114 0 0         unless (defined($filedata)) {
115             # Either a scalar filename (or nothing) was passed in, in which case
116             # we'll open it, or a filehandle was passed in, in which case we just
117             # skip the following step.
118              
119 0 0         if (ref(\$filename) ne "GLOB") {
120              
121 0 0         $filename = $self->{FILENAME} unless (defined($filename));
122              
123 0 0         unless ($filename) {
124 0           confess("No filename was specified");
125 0           $self->initialize();
126 0           return undef;
127             }
128              
129 0 0         unless ($FH = new FileHandle ("< $filename")) {
130 0           confess("Error opening $filename");
131 0           $self->initialize();
132 0           return undef;
133             }
134             }
135             else {
136 0           $FH = $filename;
137             }
138              
139             # Just to make sure...
140 0           binmode $FH;
141 0           seek($FH,0,0);
142              
143 0           $size = read ($FH, $hdr, 8);
144             }
145             else {
146 0           $hdr = substr($filedata, 0, 8);
147 0           $size = length($hdr);
148             }
149              
150 0 0         unless ($size) {
151             # confess("Error reading $filename");
152 0           $self->initialize();
153 0           return undef;
154             }
155              
156 0           $totsize += $size;
157              
158 0           ($SID, $version, $dataOffset) = unpack ("A4nn", $hdr);
159              
160 0 0 0       unless ( (($SID eq 'PSID') and (($version == 1) or ($version == 2))) or
      0        
      0        
      0        
161             (($SID eq 'RSID') and ($version == 2)) ) {
162             # Not a valid SID file recognized by this class.
163             # confess("File $filename is not a valid SID file");
164 0           $self->initialize();
165 0           return undef;
166             }
167              
168             # Valid SID file.
169              
170 0           $self->{SIDdata}{magicID} = $SID;
171 0           $self->{SIDdata}{version} = $version;
172 0           $self->{SIDdata}{dataOffset} = $dataOffset;
173              
174             # Slurp up the rest of the header.
175 0 0         unless (defined($filedata)) {
176 0           $size = read ($FH, $hdr, $dataOffset-8);
177             }
178             else {
179 0           $hdr = substr($filedata, 8, $dataOffset-8);
180 0           $size = length($hdr);
181             }
182              
183             # If the header is not as big as indicated by the dataOffset,
184             # we have a problem.
185 0 0         if ($size != ($dataOffset-8)) {
186             # confess("Error reading $filename - incorrect header");
187 0           $self->initialize();
188 0           return undef;
189             }
190              
191 0           $totsize += $size;
192              
193 0           $hdrlength = 2*5+4+32*3;
194 0           (@hdr) = unpack ("nnnnnNA32A32A32", substr($hdr,0,$hdrlength));
195              
196 0 0         if ($version > 1) {
197 0           my @temphdr;
198             # SID v2 has 4 more fields.
199 0           (@temphdr) = unpack ("nCCn", substr($hdr,$hdrlength,2+1+1+2));
200 0           push (@hdr, @temphdr);
201 0           $hdrlength += 2+1+1+2;
202             }
203             else {
204             # SID v1 doesn't have these fields.
205 0           $self->{SIDdata}{flags} = undef;
206 0           $self->{SIDdata}{startPage} = undef;
207 0           $self->{SIDdata}{pageLength} = undef;
208 0           $self->{SIDdata}{reserved} = undef;
209             }
210              
211             # Store header info.
212 0           for ($i=0; $i <= $#hdr; $i++) {
213 0           $self->{SIDdata}{$SIDfieldNames[$i+3]} = $hdr[$i];
214             }
215              
216             # Put the rest into PADDING. This might put nothing in it!
217 0           $self->{PADDING} = substr($hdr,$hdrlength);
218              
219             # Read the C64 data - can't be more than 64KB + 2 bytes load address.
220 0 0         unless (defined($filedata)) {
221 0           $size = read ($FH, $data, 65536+2);
222             }
223             else {
224 0           $data = substr($filedata, $dataOffset);
225 0           $size = length($data);
226             }
227              
228             # We allow a 0 length data.
229 0 0         unless (defined($size)) {
230             # confess("Error reading $filename");
231 0           $self->initialize();
232 0           return undef;
233             }
234              
235 0           $totsize += $size;
236              
237 0 0 0       if ((ref(\$filename) ne "GLOB") and !defined($filedata)) {
238 0           $FH->close();
239 0           $self->{FILENAME} = $filename;
240             }
241              
242 0           $self->{SIDdata}{data} = $data;
243              
244 0           $self->{FILESIZE} = $totsize;
245              
246 0           return 1;
247             }
248              
249             sub write {
250 0     0 1   my $self = shift;
251 0           my $filename;
252             my $output;
253 0           my @hdr;
254 0           my $i;
255 0           my $FH;
256              
257             # Check parameters.
258              
259 0 0 0       if (($_[0] =~ /^\-file(name)|(handle)$/i) and defined($_[1])) {
    0 0        
    0          
260 0           $filename = $_[1];
261             }
262             elsif (defined($_[0]) and !defined($_[1])) {
263 0           $filename = $_[0];
264             }
265             elsif (defined($_[0])) {
266 0           confess("Unknown parameter '$_[0]'!");
267 0           $self->initialize();
268 0           return undef;
269             }
270              
271             # Either a scalar filename (or nothing) was passed in, in which case
272             # we'll open it, or a filehandle was passed in, in which case we just
273             # skip the following step.
274              
275 0 0         if (ref(\$filename) ne "GLOB") {
276 0 0         $filename = $self->{FILENAME} unless (defined($filename));
277              
278 0 0         unless ($filename) {
279 0           confess("No filename was specified");
280 0           return undef;
281             }
282              
283 0 0         unless ($FH = new FileHandle ("> $filename")) {
284 0           confess("Couldn't write $filename");
285 0           return undef;
286             }
287             }
288             else {
289 0           $FH = $filename;
290             }
291              
292             # Just to make sure...
293 0           binmode $FH;
294 0           seek($FH,0,0);
295              
296 0 0         if ($self->{validateWrite}) {
297 0           $self->validate();
298             }
299              
300             # SID files use ISO 8859-1 encoding for textual fields, not Unicode.
301 0           foreach (qw/title author released/) {
302 0           $self->{SIDdata}{$_} = encode("latin1", $self->{SIDdata}{$_});
303             }
304              
305 0           for ($i=0; $i <= 11; $i++) {
306 0           $hdr[$i] = $self->{SIDdata}{$SIDfieldNames[$i]};
307             }
308              
309 0           $output = pack ("A4nnnnnnnNA32A32A32", @hdr);
310              
311 0           print $FH $output;
312              
313             # SID version 2 has 4 more fields.
314 0 0         if ($self->{SIDdata}{version} > 1) {
315 0           $output = pack ("nCCn", ($self->{SIDdata}{flags}, $self->{SIDdata}{startPage}, $self->{SIDdata}{pageLength}, $self->{SIDdata}{reserved}));
316 0           print $FH $output;
317             }
318              
319 0           print $FH $self->{PADDING};
320              
321 0           print $FH $self->{SIDdata}{data};
322              
323 0 0         if (ref(\$filename) ne "GLOB") {
324 0           $FH->close();
325             }
326             }
327              
328             # Notice that if no specific fieldname is given and we are in array/hash
329             # context, all fields are returned!
330             sub get {
331 0     0 1   my ($self, $fieldname) = @_;
332 0           my %SIDhash;
333             my $field;
334              
335 0           foreach $field (keys %{$self->{SIDdata}}) {
  0            
336 0           $SIDhash{$field} = $self->{SIDdata}{$field};
337             }
338              
339             # Strip off trailing NULLs.
340 0           $SIDhash{title} =~ s/\x00*$//;
341 0           $SIDhash{author} =~ s/\x00*$//;
342 0           $SIDhash{released} =~ s/\x00*$//;
343              
344 0 0         return unless (defined(wantarray()));
345              
346 0 0         unless (defined($fieldname)) {
347             # No specific fieldname is given. Assume user wants a hash of
348             # field values.
349 0 0         if (wantarray()) {
350 0           return %SIDhash;
351             }
352             else {
353 0           confess ("Nothing to get, not in array context");
354 0           return undef;
355             }
356             }
357              
358             # Backwards compatibility.
359 0 0         $fieldname = "released" if ($fieldname =~ /^copyright$/);
360 0 0         $fieldname = "title" if ($fieldname =~ /^name$/);
361              
362 0 0         unless (grep(/^$fieldname$/, @SIDfieldNames)) {
363 0           confess ("No such fieldname: $fieldname");
364 0           return undef;
365             }
366              
367 0           return $SIDhash{$fieldname};
368             }
369              
370             sub getFileName {
371 0     0 1   my $self = shift;
372              
373 0           return $self->{FILENAME};
374             }
375              
376             sub getFileSize {
377 0     0 1   my $self = shift;
378              
379 0           return $self->{FILESIZE};
380             }
381              
382             sub getRealLoadAddress {
383 0     0 1   my $self = shift;
384 0           my $REAL_LOAD_ADDRESS;
385              
386             # It's a read-only "implicit" field, so we just calculate it
387             # on the fly.
388 0 0 0       if ($self->{SIDdata}{data} and $self->{SIDdata}{loadAddress} == 0) {
389 0           $REAL_LOAD_ADDRESS = unpack("v", substr($self->{SIDdata}{data}, 0, 2));
390             }
391             else {
392 0           $REAL_LOAD_ADDRESS = $self->{SIDdata}{loadAddress};
393             }
394              
395 0           return $REAL_LOAD_ADDRESS;
396             }
397              
398             sub getSpeed($) {
399 0     0 1   my ($self, $songnumber) = @_;
400              
401 0 0 0       $songnumber = 1 if ((!defined($songnumber)) or ($songnumber < 1));
402              
403 0 0         if ($songnumber > $self->{SIDdata}{songs}) {
404 0           confess ("Song number '$songnumber' is invalid!");
405 0           return undef;
406             }
407              
408 0 0         $songnumber = 32 if ($songnumber > 32);
409              
410 0           return (($self->{SIDdata}{speed} >> ($songnumber-1)) & 0x1);
411             }
412              
413             sub getMUSPlayer {
414 0     0 1   my $self = shift;
415              
416 0 0         return undef unless (defined($self->{SIDdata}{flags}));
417              
418 0           return (($self->{SIDdata}{flags} >> $MUSPLAYER_OFFSET) & 0x1);
419             }
420              
421             sub isMUSPlayerRequired {
422 0     0 1   my $self = shift;
423              
424 0           return $self->getMUSPlayer();
425             }
426              
427             sub getPlaySID {
428 0     0 1   my $self = shift;
429              
430             # This is a PSID v2NG specific flag.
431 0 0         return undef unless (defined($self->{SIDdata}{flags}));
432 0 0         return undef if ($self->isRSID() );
433              
434 0           return (($self->{SIDdata}{flags} >> $PLAYSID_OFFSET) & 0x1);
435             }
436              
437             sub isPlaySIDSpecific {
438 0     0 1   my $self = shift;
439              
440 0           return $self->getPlaySID();
441             }
442              
443             sub isRSID {
444 0     0 1   my $self = shift;
445              
446 0           return ($self->{SIDdata}{magicID} eq 'RSID');
447             }
448              
449             sub getC64BASIC {
450 0     0 1   my $self = shift;
451              
452             # This is an RSID specific flag.
453 0 0         return undef unless (defined($self->{SIDdata}{flags}));
454 0 0         return undef unless ($self->isRSID() );
455              
456 0           return (($self->{SIDdata}{flags} >> $C64BASIC_OFFSET) & 0x1);
457             }
458              
459             sub isC64BASIC {
460 0     0 1   my $self = shift;
461              
462 0           return $self->getC64BASIC();
463             }
464              
465             sub getClock {
466 0     0 1   my $self = shift;
467              
468 0 0         return undef unless (defined($self->{SIDdata}{flags}));
469              
470 0           return (($self->{SIDdata}{flags} >> $CLOCK_OFFSET) & 0x3);
471             }
472              
473             sub getClockByName {
474 0     0 1   my $self = shift;
475 0           my $clock;
476              
477 0 0         return undef unless (defined($self->{SIDdata}{flags}));
478              
479 0           $clock = $self->getClock();
480              
481 0 0         if ($clock == 0) {
    0          
    0          
    0          
482 0           $clock = 'UNKNOWN';
483             }
484             elsif ($clock == 1) {
485 0           $clock = 'PAL';
486             }
487             elsif ($clock == 2) {
488 0           $clock = 'NTSC';
489             }
490             elsif ($clock == 3) {
491 0           $clock = 'EITHER';
492             }
493              
494 0           return $clock;
495             }
496              
497             sub getSIDModel {
498 0     0 1   my $self = shift;
499              
500 0 0         return undef unless (defined($self->{SIDdata}{flags}));
501              
502 0           return (($self->{SIDdata}{flags} >> $SIDMODEL_OFFSET) & 0x3);
503             }
504              
505             sub getSIDModelByName {
506 0     0 1   my $self = shift;
507 0           my $SIDModel;
508              
509 0 0         return undef unless (defined($self->{SIDdata}{flags}));
510              
511 0           $SIDModel = $self->getSIDModel();
512              
513 0 0         if ($SIDModel == 0) {
    0          
    0          
    0          
514 0           $SIDModel = 'UNKNOWN';
515             }
516             elsif ($SIDModel == 1) {
517 0           $SIDModel = '6581';
518             }
519             elsif ($SIDModel == 2) {
520 0           $SIDModel = '8580';
521             }
522             elsif ($SIDModel == 3) {
523 0           $SIDModel = 'EITHER';
524             }
525              
526 0           return $SIDModel;
527             }
528              
529             # Notice that you have to pass in a hash (field-value pairs)!
530             sub set(@) {
531 0     0 1   my ($self, %SIDhash) = @_;
532 0           my $fieldname;
533             my $paddinglength;
534 0           my $i;
535 0           my $version;
536 0           my $offset;
537 0           my $changePSIDSpecific = 0;
538              
539 0           foreach $fieldname (keys %SIDhash) {
540              
541             # Backwards compatibility.
542 0 0         $fieldname = "released" if ($fieldname =~ /^copyright$/);
543 0 0         $fieldname = "title" if ($fieldname =~ /^name$/);
544              
545 0 0         unless (grep(/^$fieldname$/, @SIDfieldNames)) {
546 0           confess ("No such fieldname: $fieldname");
547 0           next;
548             }
549              
550             # Do some basic sanity checking.
551              
552 0 0         if ($fieldname eq 'magicID') {
553 0 0 0       if (($SIDhash{$fieldname} ne 'PSID') and ($SIDhash{$fieldname} ne 'RSID')) {
554 0           confess ("Unrecognized magicID: $SIDhash{$fieldname}");
555 0           next;
556             }
557              
558 0 0         if ($SIDhash{$fieldname} ne $self->{SIDdata}{magicID}) {
559 0           $changePSIDSpecific = 1;
560             }
561             }
562              
563 0 0         if ($fieldname eq 'version') {
564 0 0 0       if (($SIDhash{$fieldname} != 1) and ($SIDhash{$fieldname} != 2)) {
565 0           confess ("Invalid SID version number '$version' - ignored");
566 0           next;
567             }
568             }
569              
570 0 0 0       if (($self->{SIDdata}{version} < 2) and
      0        
571             (($fieldname eq 'magicID') or ($fieldname eq 'flags') or ($fieldname eq 'reserved') or
572             ($fieldname eq 'startPage') or ($fieldname eq 'pageLength'))) {
573              
574 0           confess ("Can't change '$fieldname' when SID version is set to 1");
575 0           next;
576             }
577              
578             # SID files use ISO 8859-1 encoding for textual fields, not Unicode.
579 0 0 0       if (($fieldname eq 'title') or ($fieldname eq 'author') or ($fieldname eq 'released')) {
      0        
580 0           $SIDhash{$fieldname} = encode("latin1", $SIDhash{$fieldname});
581             }
582              
583 0           $self->{SIDdata}{$fieldname} = $SIDhash{$fieldname};
584             }
585              
586 0 0         if ($self->{SIDdata}{version} == 1) {
    0          
587             # PSID v1 values are set in stone.
588 0           $self->{SIDdata}{magicID} = 'PSID';
589 0           $self->{SIDdata}{version} = 1;
590 0           $self->{SIDdata}{dataOffset} = 0x76;
591 0           $self->{SIDdata}{flags} = undef;
592 0           $self->{SIDdata}{startPage} = undef;
593 0           $self->{SIDdata}{pageLength} = undef;
594 0           $self->{SIDdata}{reserved} = undef;
595 0           $self->{PADDING} = '';
596             }
597             elsif ($self->{SIDdata}{version} == 2) {
598             # In PSID v2NG/RSID we allow dataOffset to be larger than 0x7C.
599              
600 0           $self->{PADDING} = '';
601              
602 0 0         if ($self->{SIDdata}{dataOffset} <= 0x7C) {
603 0           $self->{SIDdata}{dataOffset} = 0x7C;
604             }
605             else {
606 0           $paddinglength = $self->{SIDdata}{dataOffset} - 0x7C;
607              
608             # Add as many zeroes as necessary.
609 0           for ($i=1; $i <= $paddinglength; $i++) {
610 0           $self->{PADDING} .= pack("C", 0x00);
611             }
612             }
613              
614             # Make sure these are not undef'd.
615 0 0         unless (defined($self->{SIDdata}{flags})) {
616 0           $self->{SIDdata}{flags} = 0;
617             }
618              
619 0 0         unless (defined($self->{SIDdata}{startPage})) {
620 0           $self->{SIDdata}{startPage} = 0;
621             }
622              
623 0 0         unless (defined($self->{SIDdata}{pageLength})) {
624 0           $self->{SIDdata}{pageLength} = 0;
625             }
626              
627 0 0         unless (defined($self->{SIDdata}{reserved})) {
628 0           $self->{SIDdata}{reserved} = 0;
629             }
630              
631 0 0         if ($changePSIDSpecific) {
632             # Zero this flag only if 'flags' is not explicitly set at the same time.
633 0 0         if (!$SIDhash{'flags'}) {
634 0 0         if ($self->isRSID() ) {
635 0           $self->setC64BASIC(0);
636             }
637             else {
638 0           $self->setPlaySID(0);
639             }
640             }
641             }
642              
643             # RSID values are set in stone.
644 0 0         if ($self->isRSID() ) {
645 0           $self->{SIDdata}{playAddress} = 0;
646 0           $self->{SIDdata}{speed} = 0;
647              
648             # The preferred way is for loadAddress to be 0. The data is
649             # prepended by those 2 bytes if it needs to be changed.
650              
651 0 0         if ($self->{SIDdata}{loadAddress} != 0) {
652 0           $self->{SIDdata}{data} = pack("v", $self->{SIDdata}{loadAddress}) . $self->{SIDdata}{data};
653 0           $self->{SIDdata}{loadAddress} = 0;
654             }
655              
656             # initAddress must be 0 if the C64 BASIC flag is set.
657 0 0         if ($self->getC64BASIC() ) {
658 0           $self->{SIDdata}{initAddress} = 0;
659             }
660             }
661             }
662              
663 0           $self->{FILESIZE} = $self->{SIDdata}{dataOffset} + length($self->{PADDING}) +
664             length($self->{SIDdata}{data});
665              
666 0           return 1;
667             }
668              
669             sub setFileName($) {
670 0     0 1   my ($self, $filename) = @_;
671              
672 0           $self->{FILENAME} = $filename;
673             }
674              
675             sub setSpeed($$) {
676 0     0 1   my ($self, $songnumber, $value) = @_;
677              
678 0 0         unless (defined($songnumber)) {
679 0           confess ("No song number was specified!");
680 0           return undef;
681             }
682              
683 0 0         unless (defined($value)) {
684 0           confess ("No speed value was specified!");
685 0           return undef;
686             }
687              
688 0 0 0       if (($songnumber > $self->{SIDdata}{songs}) or ($songnumber < 1)) {
689 0           confess ("Song number '$songnumber' is invalid!");
690 0           return undef;
691             }
692              
693 0 0 0       if (($value ne 0) and ($value ne 1)) {
694 0           confess ("Specified value '$value' is invalid!");
695 0           return undef;
696             }
697              
698 0 0         $songnumber = 32 if ($songnumber > 32);
699 0 0         $songnumber = 1 if ($songnumber < 1);
700              
701             # First, clear the bit in question.
702 0           $self->{SIDdata}{speed} &= ~(0x1 << ($songnumber-1));
703              
704             # Then set it.
705 0           $self->{SIDdata}{speed} |= ($value << ($songnumber-1));
706             }
707              
708             sub setMUSPlayer($) {
709 0     0 1   my ($self, $MUSplayer) = @_;
710              
711 0 0         unless (defined($self->{SIDdata}{flags})) {
712 0           confess ("Cannot set this field when SID version is 1!");
713 0           return undef;
714             }
715              
716 0 0 0       if (($MUSplayer ne 0) and ($MUSplayer ne 1)) {
717 0           confess ("Specified value '$MUSplayer' is invalid!");
718 0           return undef;
719             }
720              
721             # First, clear the bit in question.
722 0           $self->{SIDdata}{flags} &= ~(0x1 << $MUSPLAYER_OFFSET);
723              
724             # Then set it.
725 0           $self->{SIDdata}{flags} |= ($MUSplayer << $MUSPLAYER_OFFSET);
726             }
727              
728             sub setPlaySID($) {
729 0     0 1   my ($self, $PlaySID) = @_;
730              
731 0 0         if ($self->isRSID() ) {
732 0           confess ("Cannot set this field for RSID!");
733 0           return undef;
734             }
735              
736 0 0         unless (defined($self->{SIDdata}{flags})) {
737 0           confess ("Cannot set this field when SID version is 1!");
738 0           return undef;
739             }
740              
741 0 0 0       if (($PlaySID ne 0) and ($PlaySID ne 1)) {
742 0           confess ("Specified value '$PlaySID' is invalid!");
743 0           return undef;
744             }
745              
746             # First, clear the bit in question.
747 0           $self->{SIDdata}{flags} &= ~(0x1 << $PLAYSID_OFFSET);
748              
749             # Then set it.
750 0           $self->{SIDdata}{flags} |= ($PlaySID << $PLAYSID_OFFSET);
751             }
752              
753             sub setC64BASIC($) {
754 0     0 1   my ($self, $C64BASIC) = @_;
755              
756 0 0         unless ($self->isRSID() ) {
757 0           confess ("Cannot set this field for PSID!");
758 0           return undef;
759             }
760              
761 0 0         unless (defined($self->{SIDdata}{flags})) {
762 0           confess ("Cannot set this field when SID version is 1!");
763 0           return undef;
764             }
765              
766 0 0 0       if (($C64BASIC ne 0) and ($C64BASIC ne 1)) {
767 0           confess ("Specified value '$C64BASIC' is invalid!");
768 0           return undef;
769             }
770              
771             # First, clear the bit in question.
772 0           $self->{SIDdata}{flags} &= ~(0x1 << $C64BASIC_OFFSET);
773              
774             # Then set it.
775 0           $self->{SIDdata}{flags} |= ($C64BASIC << $C64BASIC_OFFSET);
776              
777 0 0         if ($C64BASIC) {
778 0           $self->{SIDdata}{initAddress} = 0;
779             }
780             }
781              
782             sub setClock($) {
783 0     0 1   my ($self, $clock) = @_;
784              
785 0 0         unless (defined($self->{SIDdata}{flags})) {
786 0           confess ("Cannot set this field when SID version is 1!");
787 0           return undef;
788             }
789              
790 0 0 0       if (($clock < 0) or ($clock > 3)) {
791 0           confess ("Specified value '$clock' is invalid!");
792 0           return undef;
793             }
794              
795             # First, clear the bits in question.
796 0           $self->{SIDdata}{flags} &= ~(0x3 << $CLOCK_OFFSET);
797              
798             # Then set them.
799 0           $self->{SIDdata}{flags} |= ($clock << $CLOCK_OFFSET);
800             }
801              
802             sub setClockByName($) {
803 0     0 1   my ($self, $clock) = @_;
804              
805 0 0         unless (defined($self->{SIDdata}{flags})) {
806 0           confess ("Cannot set this field when SID version is 1!");
807 0           return undef;
808             }
809              
810 0 0         if ($clock =~ /^(unknown|none|neither)$/i) {
    0          
    0          
    0          
811 0           $clock = 0;
812             }
813             elsif ($clock =~ /^PAL$/i) {
814 0           $clock = 1;
815             }
816             elsif ($clock =~ /^NTSC$/i) {
817 0           $clock = 2;
818             }
819             elsif ($clock =~ /^(any|both|either)$/i) {
820 0           $clock = 3;
821             }
822             else {
823 0           confess ("Specified value '$clock' is invalid!");
824 0           return undef;
825             }
826              
827 0           $self->setClock($clock);
828             }
829              
830             sub setSIDModel($) {
831 0     0 1   my ($self, $SIDModel) = @_;
832              
833 0 0         unless (defined($self->{SIDdata}{flags})) {
834 0           confess ("Cannot set this field when SID version is 1!");
835 0           return undef;
836             }
837              
838 0 0 0       if (($SIDModel < 0) or ($SIDModel > 3)) {
839 0           confess ("Specified value '$SIDModel' is invalid!");
840 0           return undef;
841             }
842              
843             # First, clear the bits in question.
844 0           $self->{SIDdata}{flags} &= ~(0x3 << $SIDMODEL_OFFSET);
845              
846             # Then set them.
847 0           $self->{SIDdata}{flags} |= ($SIDModel << $SIDMODEL_OFFSET);
848             }
849              
850             sub setSIDModelByName($) {
851 0     0 1   my ($self, $SIDModel) = @_;
852              
853 0 0         unless (defined($self->{SIDdata}{flags})) {
854 0           confess ("Cannot set this field when SID version is 1!");
855 0           return undef;
856             }
857              
858 0 0 0       if ($SIDModel =~ /^(unknown|none|neither)$/i) {
    0 0        
    0          
    0          
859 0           $SIDModel = 0;
860             }
861             elsif (($SIDModel =~ /^6581$/) or ($SIDModel == 6581)) {
862 0           $SIDModel = 1;
863             }
864             elsif (($SIDModel =~ /^8580$/i) or ($SIDModel == 8580)) {
865 0           $SIDModel = 2;
866             }
867             elsif ($SIDModel =~ /^(any|both|either)$/i) {
868 0           $SIDModel = 3;
869             }
870             else {
871 0           confess ("Specified value '$SIDModel' is invalid!");
872 0           return undef;
873             }
874              
875 0           $self->setSIDModel($SIDModel);
876             }
877              
878             sub getFieldNames {
879 0     0 1   my $self = shift;
880 0           my (@SIDfields) = @SIDfieldNames;
881              
882 0           return (@SIDfields);
883             }
884              
885             sub getMD5 {
886 0     0 1   my ($self, $oldMD5) = @_;
887              
888 0           my $md5 = Digest::MD5->new;
889              
890 0 0 0       if (($self->{SIDdata}{loadAddress} == 0) and $self->{SIDdata}{data}) {
891 0           $md5->add(substr($self->{SIDdata}{data},2));
892             }
893             else {
894 0           $md5->add($self->{SIDdata}{data});
895             }
896              
897 0           $md5->add(pack("v", $self->{SIDdata}{initAddress}));
898 0           $md5->add(pack("v", $self->{SIDdata}{playAddress}));
899              
900 0           my $songs = $self->{SIDdata}{songs};
901 0           $md5->add(pack("v", $songs));
902              
903 0           my $speed = $self->{SIDdata}{speed};
904              
905 0           for (my $i=0; $i < $songs; $i++) {
906 0           my $speedFlag;
907 0 0 0       if ( (($speed & (1 << $i)) != 0) or ($self->isRSID() ) ) {
908 0           $speedFlag = 60;
909             }
910             else {
911 0           $speedFlag = 0;
912             }
913 0           $md5->add(pack("C",$speedFlag));
914             }
915              
916 0           my $clock = $self->getClock();
917              
918 0 0 0       if (($self->{SIDdata}{version} > 1) and ($clock == 2) and !$oldMD5) {
      0        
919 0           $md5->add(pack("C",$clock));
920             }
921              
922 0           return ($md5->hexdigest);
923             }
924              
925             sub alwaysValidateWrite($) {
926 0     0 1   my ($self, $setting) = @_;
927              
928 0           $self->{validateWrite} = $setting;
929             }
930              
931             sub validate {
932 0     0 1   my $self = shift;
933 0           my $field;
934             my $MUSPlayer;
935 0           my $PlaySID;
936 0           my $C64BASIC;
937 0           my $clock;
938 0           my $SIDModel;
939              
940             # Change to version v2.
941 0 0         if ($self->{SIDdata}{version} < 2) {
942             # carp ("Changing SID to v2");
943 0           $self->{SIDdata}{version} = 2;
944             }
945              
946 0 0         if ($self->isRSID() ) {
947 0           $self->{SIDdata}{playAddress} = 0;
948 0           $self->{SIDdata}{speed} = 0;
949             }
950              
951 0 0         if ($self->{SIDdata}{dataOffset} != 0x7C) {
952 0           $self->{SIDdata}{dataOffset} = 0x7C;
953             # carp ("'dataOffset' was not 0x007C - set to 0x007C");
954             }
955              
956             # Sanity check the fields.
957              
958             # Textual fields can't be longer than 31 chars.
959 0           foreach $field (qw(title author released)) {
960              
961             # Strip trailing whitespace.
962 0           $self->{SIDdata}{$field} =~ s/\s+$//;
963              
964             # Convert to ISO 8859-1 ASCII.
965 0           $self->{SIDdata}{$field} = encode("latin1", $self->{SIDdata}{$field});
966              
967             # Take off any superfluous null-padding.
968 0           $self->{SIDdata}{$field} =~ s/\x00+$//;
969              
970 0 0         if (length($self->{SIDdata}{$field}) > 31) {
971 0           $self->{SIDdata}{$field} = substr($self->{SIDdata}{$field}, 0, 31);
972             # carp ("'$field' field was longer than 31 chars - chopped to 31");
973             }
974             }
975              
976             # If this is an RSID, initAddress shouldn't be pointing to a ROM memory
977             # area, or be outside the load range. Also, if the C64 BASIC flag is set,
978             # initAddress must be 0.
979              
980 0 0 0       if ( ($self->isRSID() ) and
      0        
981             ( ((($self->{SIDdata}{initAddress} > 0) and ($self->{SIDdata}{initAddress} < 0x07E8)) or
982             (($self->{SIDdata}{initAddress} >= 0xA000) and ($self->{SIDdata}{initAddress} < 0xC000)) or
983             (($self->{SIDdata}{initAddress} >= 0xD000) and ($self->{SIDdata}{initAddress} <= 0xFFFF)) or
984             ($self->{SIDdata}{initAddress} < $self->getRealLoadAddress()) or
985             ($self->{SIDdata}{initAddress} > ($self->getRealLoadAddress() + length($self->{SIDdata}{data}) - 3))
986             ) or
987             ($self->getC64BASIC() )
988             )
989             ) {
990              
991 0           $self->{SIDdata}{initAddress} = 0;
992              
993             # carp ("'initAddress' was invalid - set to 0");
994             }
995              
996             # The preferred way is for loadAddress to be 0. It also shouldn't be less
997             # than 0x07E8 in RSID files. The data is prepended by those 2 bytes if it
998             # needs to be changed.
999              
1000 0 0 0       if ($self->{SIDdata}{loadAddress} != 0) {
    0          
1001              
1002             # Load address must not be less than 0x07E8 in RSID files.
1003 0 0 0       if (($self->isRSID() ) and
1004             ($self->{SIDdata}{loadAddress} < 0x07E8) ) {
1005              
1006 0           $self->{SIDdata}{loadAddress} = 0x07E8;
1007             }
1008              
1009 0           $self->{SIDdata}{data} = pack("v", $self->{SIDdata}{loadAddress}) . $self->{SIDdata}{data};
1010 0           $self->{SIDdata}{loadAddress} = 0;
1011             # carp ("'loadAddress' was non-zero - set to 0");
1012             }
1013             elsif (($self->isRSID() ) and
1014             ($self->getRealLoadAddress() < 0x07E8) ) {
1015              
1016 0           $self->{SIDdata}{data} = pack("v", 0x07E8) . substr($self->{SIDdata}{data}, 2);
1017             }
1018              
1019             # If this is a PSID, initAddress shouldn't be outside the load range.
1020              
1021 0 0 0       if ( ($self->isRSID() ) and
      0        
1022             (($self->{SIDdata}{initAddress} < $self->getRealLoadAddress()) or
1023             ($self->{SIDdata}{initAddress} > ($self->getRealLoadAddress() + length($self->{SIDdata}{data}) - 3))
1024             )
1025             ) {
1026              
1027 0           $self->{SIDdata}{initAddress} = 0;
1028              
1029             # carp ("'initAddress' was invalid - set to 0");
1030             }
1031              
1032             # These fields should better be in the 0x0000-0xFFFF range!
1033 0           foreach $field (qw(loadAddress initAddress playAddress)) {
1034 0 0 0       if (($self->{SIDdata}{$field} < 0) or ($self->{SIDdata}{$field} > 0xFFFF)) {
1035             # confess ("'$field' value of $self->{SIDdata}{$field} is out of range");
1036 0           $self->{SIDdata}{$field} = 0;
1037             }
1038             }
1039              
1040             # These fields should better be in the 0x00-0xFF range!
1041 0           foreach $field (qw(startPage pageLength)) {
1042 0 0 0       if (!defined($self->{SIDdata}{$field}) or ($self->{SIDdata}{$field} < 0) or ($self->{SIDdata}{$field} > 0xFF)) {
      0        
1043             # confess ("'$field' value of $self->{SIDdata}{$field} is out of range");
1044 0           $self->{SIDdata}{$field} = 0;
1045             }
1046             }
1047              
1048             # This field's max is 256.
1049 0 0         if ($self->{SIDdata}{songs} > 256) {
1050 0           $self->{SIDdata}{songs} = 256;
1051             # carp ("'songs' was more than 256 - set to 256");
1052             }
1053              
1054             # This field's min is 1.
1055 0 0         if ($self->{SIDdata}{songs} < 1) {
1056 0           $self->{SIDdata}{songs} = 1;
1057             # carp ("'songs' was less than 1 - set to 1");
1058             }
1059              
1060             # If an invalid startSong is specified, set it to 1.
1061 0 0         if ($self->{SIDdata}{startSong} > $self->{SIDdata}{songs}) {
1062 0           $self->{SIDdata}{startSong} = 1;
1063             # carp ("Invalid 'startSong' field - set to 1");
1064             }
1065              
1066 0 0         unless ($self->isRSID() ) {
1067             # Only the relevant fields in 'speed' will be set.
1068 0           my $tempSpeed = 0;
1069 0           my $maxSongs = $self->{SIDdata}{songs};
1070              
1071             # There are only 32 bits in speed.
1072 0 0         if ($maxSongs > 32) {
1073 0           $maxSongs = 32;
1074             }
1075              
1076 0           for (my $i=0; $i < $maxSongs; $i++) {
1077 0           $tempSpeed += ($self->{SIDdata}{speed} & (1 << $i));
1078             }
1079 0           $self->{SIDdata}{speed} = $tempSpeed;
1080             }
1081              
1082 0 0         unless (defined($self->{SIDdata}{flags})) {
1083 0           $self->{SIDdata}{flags} = 0;
1084             }
1085             else {
1086             # Only the relevant fields in 'flags' will be set.
1087 0           $MUSPlayer = $self->isMUSPlayerRequired();
1088 0           $clock = $self->getClock();
1089 0           $SIDModel = $self->getSIDModel();
1090              
1091 0 0         unless ($self->isRSID() ) {
1092 0           $PlaySID = $self->isPlaySIDSpecific();
1093             }
1094             else {
1095 0           $C64BASIC = $self->isC64BASIC();
1096             }
1097              
1098 0           $self->{SIDdata}{flags} = 0;
1099              
1100 0           $self->setMUSPlayer($MUSPlayer);
1101 0           $self->setClock($clock);
1102 0           $self->setSIDModel($SIDModel);
1103              
1104 0 0         unless ($self->isRSID() ) {
1105 0           $self->setPlaySID($PlaySID);
1106             }
1107             else {
1108 0           $self->setC64BASIC($C64BASIC);
1109             }
1110             }
1111              
1112 0 0 0       if (($self->{SIDdata}{startPage} == 0) or ($self->{SIDdata}{startPage} == 0xFF)) {
    0          
    0          
1113 0           $self->{SIDdata}{pageLength} = 0;
1114             }
1115             elsif ((($self->{SIDdata}{startPage} << 8) + ($self->{SIDdata}{pageLength} << 8) - 1) > 0xFFFF) {
1116 0           $self->{SIDdata}{pageLength} = 0xFF - $self->{SIDdata}{startPage};
1117             }
1118             elsif ($self->{SIDdata}{pageLength} == 0) {
1119 0           $self->{SIDdata}{pageLength} = 1;
1120             }
1121              
1122             # Reloc info must not overlap or encompass the ROM/IO and
1123             # reserved memory areas.
1124              
1125             # Is startPage within the ROM or reserved memory areas?
1126 0 0 0       if ( (($self->{SIDdata}{startPage} >= 0xA0) and ($self->{SIDdata}{startPage} < 0xC0)) or
      0        
      0        
      0        
      0        
1127             (($self->{SIDdata}{startPage} >= 0xD0) and ($self->{SIDdata}{startPage} < 0xFF)) or
1128             (($self->{SIDdata}{startPage} > 0x00) and ($self->{SIDdata}{startPage} < 0x04)) ) {
1129              
1130 0           $self->{SIDdata}{startPage} = 0xFF;
1131 0           $self->{SIDdata}{pageLength} = 0x00;
1132             }
1133              
1134             # Is the end of the relocation range within the ROM or reserved memory areas?
1135 0 0 0       if ( (( ($self->{SIDdata}{startPage} << 8) + ($self->{SIDdata}{pageLength} << 8) - 1 >= 0xA000) and ( ($self->{SIDdata}{startPage} << 8) + ($self->{SIDdata}{pageLength} << 8) - 1 < 0xC000)) or
      0        
      0        
      0        
      0        
1136             (( ($self->{SIDdata}{startPage} << 8) + ($self->{SIDdata}{pageLength} << 8) - 1 >= 0xD000) and ( ($self->{SIDdata}{startPage} << 8) + ($self->{SIDdata}{pageLength} << 8) - 1 <= 0xFFFF)) or
1137             (( ($self->{SIDdata}{startPage} << 8) + ($self->{SIDdata}{pageLength} << 8) - 1 > 0x0000) and ( ($self->{SIDdata}{startPage} << 8) + ($self->{SIDdata}{pageLength} << 8) - 1 < 0x0400)) ) {
1138              
1139 0           $self->{SIDdata}{startPage} = 0xFF;
1140 0           $self->{SIDdata}{pageLength} = 0x00;
1141             }
1142              
1143             # Does the relocation range encompass a ROM area?
1144 0 0 0       if ( ($self->{SIDdata}{startPage} < 0xA0) and (($self->{SIDdata}{startPage} << 8) + ($self->{SIDdata}{pageLength} << 8) - 1 >= 0xC000) ) {
1145              
1146 0           $self->{SIDdata}{startPage} = 0xFF;
1147 0           $self->{SIDdata}{pageLength} = 0x00;
1148             }
1149              
1150             # Relocation range must not overlap or encompass the load range.
1151              
1152 0 0 0       if ( (($self->{SIDdata}{startPage} << 8) >= $self->getRealLoadAddress()) and
1153             (($self->{SIDdata}{startPage} << 8) <= ($self->getRealLoadAddress() + length($self->{SIDdata}{data}) - 3)
1154             ) ) {
1155              
1156 0           $self->{SIDdata}{startPage} = 0xFF;
1157 0           $self->{SIDdata}{pageLength} = 0x00;
1158             }
1159              
1160 0 0 0       if ( (($self->{SIDdata}{startPage} << 8) + ($self->{SIDdata}{pageLength} << 8) - 1 >= $self->getRealLoadAddress()) and
1161             (($self->{SIDdata}{startPage} << 8) + ($self->{SIDdata}{pageLength} << 8) - 1 <= ($self->getRealLoadAddress() + length($self->{SIDdata}{data}) - 3))
1162             ) {
1163              
1164 0           $self->{SIDdata}{startPage} = 0xFF;
1165 0           $self->{SIDdata}{pageLength} = 0x00;
1166             }
1167              
1168 0 0 0       if ( (($self->{SIDdata}{startPage} << 8) < $self->getRealLoadAddress()) and
1169             (($self->{SIDdata}{startPage} << 8) + ($self->{SIDdata}{pageLength} << 8) - 1 > ($self->getRealLoadAddress() + length($self->{SIDdata}{data}) - 3))
1170             ) {
1171              
1172 0           $self->{SIDdata}{startPage} = 0xFF;
1173 0           $self->{SIDdata}{pageLength} = 0x00;
1174             }
1175              
1176 0           $self->{SIDdata}{reserved} = 0;
1177              
1178             # The preferred way is to have no padding between the v2 header and the
1179             # C64 data.
1180 0 0         if ($self->{PADDING}) {
1181 0           $self->{PADDING} = '';
1182             # carp ("Invalid bytes were between the header and the data - removed them");
1183             }
1184              
1185             # Recalculate size.
1186 0           $self->{FILESIZE} = $self->{SIDdata}{dataOffset} + length($self->{PADDING}) +
1187             length($self->{SIDdata}{data});
1188             }
1189              
1190             1;
1191              
1192             __END__